Merge branch 'master' into jgrpp
# Conflicts: # src/string.cpp
This commit is contained in:
@@ -285,11 +285,29 @@ static const uint NUM_SONGS_AVAILABLE = 1 + NUM_SONG_CLASSES * NUM_SONGS_CLASS;
|
||||
/** Maximum number of songs in the (custom) playlist */
|
||||
static const uint NUM_SONGS_PLAYLIST = 32;
|
||||
|
||||
/* Functions to read DOS music CAT files, similar to but not quite the same as sound effect CAT files */
|
||||
char *GetMusicCatEntryName(const char *filename, size_t entrynum);
|
||||
byte *GetMusicCatEntryData(const char *filename, size_t entrynum, size_t &entrylen);
|
||||
|
||||
enum MusicTrackType {
|
||||
MTT_STANDARDMIDI, ///< Standard MIDI file
|
||||
MTT_MPSMIDI, ///< MPS GM driver MIDI format (contained in a CAT file)
|
||||
};
|
||||
|
||||
/** Metadata about a music track. */
|
||||
struct MusicSongInfo {
|
||||
char songname[32]; ///< name of song displayed in UI
|
||||
byte tracknr; ///< track number of song displayed in UI
|
||||
const char *filename; ///< file on disk containing song (when used in MusicSet class, this pointer is owned by MD5File object for the file)
|
||||
MusicTrackType filetype; ///< decoder required for song file
|
||||
int cat_index; ///< entry index in CAT file, for filetype==MTT_MPSMIDI
|
||||
};
|
||||
|
||||
/** All data of a music set. */
|
||||
struct MusicSet : BaseSet<MusicSet, NUM_SONGS_AVAILABLE, false> {
|
||||
/** The name of the different songs. */
|
||||
char song_name[NUM_SONGS_AVAILABLE][32];
|
||||
byte track_nr[NUM_SONGS_AVAILABLE];
|
||||
/** Data about individual songs in set. */
|
||||
MusicSongInfo songinfo[NUM_SONGS_AVAILABLE];
|
||||
/** Number of valid songs in set. */
|
||||
byte num_available;
|
||||
|
||||
bool FillSetDetails(struct IniFile *ini, const char *path, const char *full_filename);
|
||||
|
@@ -532,7 +532,7 @@ FILE *FioFOpenFile(const char *filename, const char *mode, Subdirectory subdir,
|
||||
* Create a directory with the given name
|
||||
* @param name the new name of the directory
|
||||
*/
|
||||
static void FioCreateDirectory(const char *name)
|
||||
void FioCreateDirectory(const char *name)
|
||||
{
|
||||
/* Ignore directory creation errors; they'll surface later on, and most
|
||||
* of the time they are 'directory already exists' errors anyhow. */
|
||||
|
@@ -55,6 +55,7 @@ char *FioGetFullPath(char *buf, const char *last, Searchpath sp, Subdirectory su
|
||||
char *FioFindFullPath(char *buf, const char *last, Subdirectory subdir, const char *filename);
|
||||
char *FioAppendDirectory(char *buf, const char *last, Searchpath sp, Subdirectory subdir);
|
||||
char *FioGetDirectory(char *buf, const char *last, Subdirectory subdir);
|
||||
void FioCreateDirectory(const char *name);
|
||||
|
||||
const char *FiosGetScreenshotDir();
|
||||
|
||||
|
@@ -75,6 +75,7 @@ public:
|
||||
virtual GlyphID MapCharToGlyph(WChar key) { assert(IsPrintable(key)); return SPRITE_GLYPH | key; }
|
||||
virtual const void *GetFontTable(uint32 tag, size_t &length) { length = 0; return NULL; }
|
||||
virtual const char *GetFontName() { return "sprite"; }
|
||||
virtual bool IsBuiltInFont() { return true; }
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -254,6 +255,7 @@ public:
|
||||
virtual GlyphID MapCharToGlyph(WChar key);
|
||||
virtual const void *GetFontTable(uint32 tag, size_t &length);
|
||||
virtual const char *GetFontName() { return face->family_name; }
|
||||
virtual bool IsBuiltInFont() { return false; }
|
||||
};
|
||||
|
||||
FT_Library _library = NULL;
|
||||
|
@@ -147,6 +147,11 @@ public:
|
||||
{
|
||||
return this->parent != NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a built-in sprite font?
|
||||
*/
|
||||
virtual bool IsBuiltInFont() = 0;
|
||||
};
|
||||
|
||||
/** Get the SpriteID mapped to the given font size and key */
|
||||
|
@@ -21,6 +21,10 @@
|
||||
#include <unicode/ustring.h>
|
||||
#endif /* WITH_ICU_LAYOUT */
|
||||
|
||||
#ifdef WITH_UNISCRIBE
|
||||
#include "os/windows/string_uniscribe.h"
|
||||
#endif /* WITH_UNISCRIBE */
|
||||
|
||||
#include "safeguards.h"
|
||||
|
||||
|
||||
@@ -113,26 +117,12 @@ le_bool Font::getGlyphPoint(LEGlyphID glyph, le_int32 pointNumber, LEPoint &poin
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static size_t AppendToBuffer(UChar *buff, const UChar *buffer_last, WChar c)
|
||||
{
|
||||
/* Transform from UTF-32 to internal ICU format of UTF-16. */
|
||||
int32 length = 0;
|
||||
UErrorCode err = U_ZERO_ERROR;
|
||||
u_strFromUTF32(buff, buffer_last - buff, &length, (UChar32*)&c, 1, &err);
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for doing layouts with ICU.
|
||||
*/
|
||||
class ICUParagraphLayout : public AutoDeleteSmallVector<ParagraphLayouter::Line *, 4>, public ParagraphLayouter {
|
||||
ParagraphLayout *p; ///< The actual ICU paragraph layout.
|
||||
public:
|
||||
/** Helper for GetLayouter, to get the right type. */
|
||||
typedef UChar CharType;
|
||||
/** Helper for GetLayouter, to get whether the layouter supports RTL. */
|
||||
static const bool SUPPORTS_RTL = true;
|
||||
|
||||
/** Visual run contains data about the bit of text with the same font. */
|
||||
class ICUVisualRun : public ParagraphLayouter::VisualRun {
|
||||
const ParagraphLayout::VisualRun *vr; ///< The actual ICU vr.
|
||||
@@ -184,35 +174,54 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
static ParagraphLayouter *GetParagraphLayout(UChar *buff, UChar *buff_end, FontMap &fontMapping)
|
||||
{
|
||||
int32 length = buff_end - buff;
|
||||
/**
|
||||
* Helper class to construct a new #ICUParagraphLayout.
|
||||
*/
|
||||
class ICUParagraphLayoutFactory {
|
||||
public:
|
||||
/** Helper for GetLayouter, to get the right type. */
|
||||
typedef UChar CharType;
|
||||
/** Helper for GetLayouter, to get whether the layouter supports RTL. */
|
||||
static const bool SUPPORTS_RTL = true;
|
||||
|
||||
if (length == 0) {
|
||||
/* ICU's ParagraphLayout cannot handle empty strings, so fake one. */
|
||||
buff[0] = ' ';
|
||||
length = 1;
|
||||
fontMapping.End()[-1].first++;
|
||||
static ParagraphLayouter *GetParagraphLayout(UChar *buff, UChar *buff_end, FontMap &fontMapping)
|
||||
{
|
||||
int32 length = buff_end - buff;
|
||||
|
||||
if (length == 0) {
|
||||
/* ICU's ParagraphLayout cannot handle empty strings, so fake one. */
|
||||
buff[0] = ' ';
|
||||
length = 1;
|
||||
fontMapping.End()[-1].first++;
|
||||
}
|
||||
|
||||
/* Fill ICU's FontRuns with the right data. */
|
||||
FontRuns runs(fontMapping.Length());
|
||||
for (FontMap::iterator iter = fontMapping.Begin(); iter != fontMapping.End(); iter++) {
|
||||
runs.add(iter->second, iter->first);
|
||||
}
|
||||
|
||||
LEErrorCode status = LE_NO_ERROR;
|
||||
/* ParagraphLayout does not copy "buff", so it must stay valid.
|
||||
* "runs" is copied according to the ICU source, but the documentation does not specify anything, so this might break somewhen. */
|
||||
ParagraphLayout *p = new ParagraphLayout(buff, length, &runs, NULL, NULL, NULL, _current_text_dir == TD_RTL ? UBIDI_DEFAULT_RTL : UBIDI_DEFAULT_LTR, false, status);
|
||||
if (status != LE_NO_ERROR) {
|
||||
delete p;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return new ICUParagraphLayout(p);
|
||||
}
|
||||
|
||||
/* Fill ICU's FontRuns with the right data. */
|
||||
FontRuns runs(fontMapping.Length());
|
||||
for (FontMap::iterator iter = fontMapping.Begin(); iter != fontMapping.End(); iter++) {
|
||||
runs.add(iter->second, iter->first);
|
||||
static size_t AppendToBuffer(UChar *buff, const UChar *buffer_last, WChar c)
|
||||
{
|
||||
/* Transform from UTF-32 to internal ICU format of UTF-16. */
|
||||
int32 length = 0;
|
||||
UErrorCode err = U_ZERO_ERROR;
|
||||
u_strFromUTF32(buff, buffer_last - buff, &length, (UChar32*)&c, 1, &err);
|
||||
return length;
|
||||
}
|
||||
|
||||
LEErrorCode status = LE_NO_ERROR;
|
||||
/* ParagraphLayout does not copy "buff", so it must stay valid.
|
||||
* "runs" is copied according to the ICU source, but the documentation does not specify anything, so this might break somewhen. */
|
||||
ParagraphLayout *p = new ParagraphLayout(buff, length, &runs, NULL, NULL, NULL, _current_text_dir == TD_RTL ? UBIDI_DEFAULT_RTL : UBIDI_DEFAULT_LTR, false, status);
|
||||
if (status != LE_NO_ERROR) {
|
||||
delete p;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return new ICUParagraphLayout(p);
|
||||
}
|
||||
|
||||
};
|
||||
#endif /* WITH_ICU_LAYOUT */
|
||||
|
||||
/*** Paragraph layout ***/
|
||||
@@ -236,11 +245,6 @@ static ParagraphLayouter *GetParagraphLayout(UChar *buff, UChar *buff_end, FontM
|
||||
*/
|
||||
class FallbackParagraphLayout : public ParagraphLayouter {
|
||||
public:
|
||||
/** Helper for GetLayouter, to get the right type. */
|
||||
typedef WChar CharType;
|
||||
/** Helper for GetLayouter, to get whether the layouter supports RTL. */
|
||||
static const bool SUPPORTS_RTL = false;
|
||||
|
||||
/** Visual run contains data about the bit of text with the same font. */
|
||||
class FallbackVisualRun : public ParagraphLayouter::VisualRun {
|
||||
Font *font; ///< The font used to layout these.
|
||||
@@ -280,6 +284,42 @@ public:
|
||||
const ParagraphLayouter::Line *NextLine(int max_width);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper class to construct a new #FallbackParagraphLayout.
|
||||
*/
|
||||
class FallbackParagraphLayoutFactory {
|
||||
public:
|
||||
/** Helper for GetLayouter, to get the right type. */
|
||||
typedef WChar CharType;
|
||||
/** Helper for GetLayouter, to get whether the layouter supports RTL. */
|
||||
static const bool SUPPORTS_RTL = false;
|
||||
|
||||
/**
|
||||
* Get the actual ParagraphLayout for the given buffer.
|
||||
* @param buff The begin of the buffer.
|
||||
* @param buff_end The location after the last element in the buffer.
|
||||
* @param fontMapping THe mapping of the fonts.
|
||||
* @return The ParagraphLayout instance.
|
||||
*/
|
||||
static ParagraphLayouter *GetParagraphLayout(WChar *buff, WChar *buff_end, FontMap &fontMapping)
|
||||
{
|
||||
return new FallbackParagraphLayout(buff, buff_end - buff, fontMapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a wide character to the internal buffer.
|
||||
* @param buff The buffer to append to.
|
||||
* @param buffer_last The end of the buffer.
|
||||
* @param c The character to add.
|
||||
* @return The number of buffer spaces that were used.
|
||||
*/
|
||||
static size_t AppendToBuffer(WChar *buff, const WChar *buffer_last, WChar c)
|
||||
{
|
||||
*buff = c;
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the visual run.
|
||||
* @param font The font to use for this run.
|
||||
@@ -536,31 +576,6 @@ const ParagraphLayouter::Line *FallbackParagraphLayout::NextLine(int max_width)
|
||||
return l;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appand a wide character to the internal buffer.
|
||||
* @param buff The buffer to append to.
|
||||
* @param buffer_last The end of the buffer.
|
||||
* @param c The character to add.
|
||||
* @return The number of buffer spaces that were used.
|
||||
*/
|
||||
static size_t AppendToBuffer(WChar *buff, const WChar *buffer_last, WChar c)
|
||||
{
|
||||
*buff = c;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual ParagraphLayout for the given buffer.
|
||||
* @param buff The begin of the buffer.
|
||||
* @param buff_end The location after the last element in the buffer.
|
||||
* @param fontMapping THe mapping of the fonts.
|
||||
* @return The ParagraphLayout instance.
|
||||
*/
|
||||
static FallbackParagraphLayout *GetParagraphLayout(WChar *buff, WChar *buff_end, FontMap &fontMapping)
|
||||
{
|
||||
return new FallbackParagraphLayout(buff, buff_end - buff, fontMapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for getting a ParagraphLayouter of the given type.
|
||||
*
|
||||
@@ -605,7 +620,7 @@ static inline void GetLayouter(Layouter::LineCacheItem &line, const char *&str,
|
||||
* will not be handled in the fallback non ICU case because they are
|
||||
* mostly needed for RTL languages which need more ICU support. */
|
||||
if (!T::SUPPORTS_RTL && IsTextDirectionChar(c)) continue;
|
||||
buff += AppendToBuffer(buff, buffer_last, c);
|
||||
buff += T::AppendToBuffer(buff, buffer_last, c);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -621,7 +636,7 @@ static inline void GetLayouter(Layouter::LineCacheItem &line, const char *&str,
|
||||
if (!fontMapping.Contains(buff - buff_begin)) {
|
||||
fontMapping.Insert(buff - buff_begin, f);
|
||||
}
|
||||
line.layout = GetParagraphLayout(buff_begin, buff, fontMapping);
|
||||
line.layout = T::GetParagraphLayout(buff_begin, buff, fontMapping);
|
||||
line.state_after = state;
|
||||
}
|
||||
|
||||
@@ -654,11 +669,11 @@ Layouter::Layouter(const char *str, int maxw, TextColour colour, FontSize fontsi
|
||||
line.layout->Reflow();
|
||||
} else {
|
||||
/* Line is new, layout it */
|
||||
#ifdef WITH_ICU_LAYOUT
|
||||
FontState old_state = state;
|
||||
const char *old_str = str;
|
||||
|
||||
GetLayouter<ICUParagraphLayout>(line, str, state);
|
||||
#ifdef WITH_ICU_LAYOUT
|
||||
GetLayouter<ICUParagraphLayoutFactory>(line, str, state);
|
||||
if (line.layout == NULL) {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
@@ -668,11 +683,22 @@ Layouter::Layouter(const char *str, int maxw, TextColour colour, FontSize fontsi
|
||||
|
||||
state = old_state;
|
||||
str = old_str;
|
||||
GetLayouter<FallbackParagraphLayout>(line, str, state);
|
||||
}
|
||||
#else
|
||||
GetLayouter<FallbackParagraphLayout>(line, str, state);
|
||||
#endif
|
||||
|
||||
#ifdef WITH_UNISCRIBE
|
||||
if (line.layout == NULL) {
|
||||
GetLayouter<UniscribeParagraphLayoutFactory>(line, str, state);
|
||||
if (line.layout == NULL) {
|
||||
state = old_state;
|
||||
str = old_str;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (line.layout == NULL) {
|
||||
GetLayouter<FallbackParagraphLayoutFactory>(line, str, state);
|
||||
}
|
||||
}
|
||||
|
||||
/* Copy all lines into a local cache so we can reuse them later on more easily. */
|
||||
@@ -809,6 +835,10 @@ void Layouter::ResetFontCache(FontSize size)
|
||||
|
||||
/* We must reset the linecache since it references the just freed fonts */
|
||||
ResetLineCache();
|
||||
|
||||
#if defined(WITH_UNISCRIBE)
|
||||
UniscribeResetScriptCache(size);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2972,8 +2972,6 @@ STR_TOWN_POPULATION :{BLACK}Wêreldb
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (City)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Bevolking: {ORANGE}{COMMA}{BLACK} Huise: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passasiers verlede maand: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Pos verlede maand: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Vrag nodig om dorp te laat groei:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} vereis
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} vereis in winter
|
||||
|
@@ -2587,8 +2587,6 @@ STR_TOWN_POPULATION :{BLACK}سكان
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} - مدينة -
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}السكان: {ORANGE}{COMMA}{BLACK} المنازل: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}الركاب الشهر الماضي: {ORANGE}{COMMA}{BLACK} الأقصى: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}طرود البريد الشهر الماضي: {ORANGE}{COMMA}{BLACK} الأقصى: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK} نمو المدينة يتطلب بضائع
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} مطلوب
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK}مطلوب في الشتاء
|
||||
|
@@ -2861,8 +2861,6 @@ STR_TOWN_POPULATION :{BLACK}Munduko
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Hiria)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Biztanleria: {ORANGE}{COMMA}{BLACK} Etxe kopurua: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Bidaiari kopurua aurreko hilabetean: {ORANGE}{COMMA}{BLACK} gehienez: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Posta kopurua aurreko hilabetean: {ORANGE}{COMMA}{BLACK} gehienez: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Beharrezko zama herri hazkunderako:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} beharrezkoa
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} Neguan beharrezkoa
|
||||
|
@@ -3318,8 +3318,6 @@ STR_TOWN_POPULATION :{BLACK}Насе
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Мэґаполіс)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Насельніцтва: {ORANGE}{COMMA}{BLACK} Будынкаў: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Пасажыраў за мінулы месяц: {ORANGE}{COMMA}{BLACK} Макс.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Пошты за мінулы месяц: {ORANGE}{COMMA}{BLACK} Макс.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Груз, неабходны для росту горада:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} патрабу{G 0 е e e ю}цца
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} патрабу{G 0 е e e ю}цца ўзімку
|
||||
|
@@ -2976,8 +2976,6 @@ STR_TOWN_POPULATION :{BLACK}Populaç
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Cidade)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}População: {ORANGE}{COMMA}{BLACK} Casas: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passageiros no mês passado: {ORANGE}{COMMA}{BLACK} máx: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Cartas no mês passado: {ORANGE}{COMMA}{BLACK} máx: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Carga necessária para prover o crescimento:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} necessário(a)
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} necessário no inverno
|
||||
|
@@ -2909,8 +2909,6 @@ STR_TOWN_POPULATION :{BLACK}Обща
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Град)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Население: {ORANGE}{COMMA}{BLACK} Жилища: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Пътници през последния месец: {ORANGE}{COMMA}{BLACK} максимум: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Поща през последния месец: {ORANGE}{COMMA}{BLACK} максимум: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Товар нужен за растеж на града:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} необходим
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} необходим през зимата
|
||||
|
@@ -2976,8 +2976,6 @@ STR_TOWN_POPULATION :{BLACK}Poblaci
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Ciutat)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Població: {ORANGE}{COMMA}{BLACK} Cases: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passatgers el darrer mes: {ORANGE}{COMMA}{BLACK} màx: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Correu el darrer mes: {ORANGE}{COMMA}{BLACK} màx: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Càrrega requerida per tal que la població creixi:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} requerides
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} requerit a l'hivern
|
||||
|
@@ -3081,8 +3081,6 @@ STR_TOWN_POPULATION :{BLACK}Svjetsko
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Metropola)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Stanovništvo: {ORANGE}{COMMA}{BLACK} Kuće: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Putnika prošli mjesec: {ORANGE}{COMMA}{BLACK} najviše: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Pošte prošli mjesec: {ORANGE}{COMMA}{BLACK} najviše: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Potrebno tereta za rast grada:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} potrebno
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} potrebno zimi
|
||||
|
@@ -3068,8 +3068,6 @@ STR_TOWN_POPULATION :{BLACK}Populace
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (velkoměsto)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Populace: {ORANGE}{COMMA}{BLACK} Domů: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Cestujících minulý měsíc: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Pošta za minulý měsíc: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Množství doručeného nákladu potřebného pro rozvoj města:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{RED}Je potřeba {ORANGE}{STRING}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} vyžadováno v zimě
|
||||
|
@@ -2976,8 +2976,6 @@ STR_TOWN_POPULATION :{BLACK}Verdens
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (by)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Indbyggere: {ORANGE}{COMMA}{BLACK} Huse: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passagerer sidste måned: {ORANGE}{COMMA}{BLACK} maks.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post sidste måned: {ORANGE}{COMMA}{BLACK} maks.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Nødvendig godsmængde for at byen kan vokse:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} krævet
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} kræves om vinteren
|
||||
|
@@ -2975,8 +2975,6 @@ STR_TOWN_POPULATION :{BLACK}Wereldbe
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Groeistad)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Bevolking: {ORANGE}{COMMA}{BLACK} Huizen: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passagiers afgelopen maand: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post afgelopen maand: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Vracht nodig voor groei van stad:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} noodzakelijk
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} noodzakelijk in de winter
|
||||
|
@@ -3457,8 +3457,7 @@ STR_TOWN_POPULATION :{BLACK}World po
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (City)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Population: {ORANGE}{COMMA}{BLACK} Houses: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passengers last month: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Mail last month: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_LAST_MONTH_MAX :{BLACK}{CARGO_LIST} last month: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Cargo needed for town growth:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} required
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} required in winter
|
||||
@@ -4998,6 +4997,7 @@ STR_ERROR_DRIVE_THROUGH_ON_TOWN_ROAD :{WHITE}... this
|
||||
STR_ERROR_DRIVE_THROUGH_DIRECTION :{WHITE}... road facing in the wrong direction
|
||||
STR_ERROR_DRIVE_THROUGH_CORNER :{WHITE}... drive through stops can't have corners
|
||||
STR_ERROR_DRIVE_THROUGH_JUNCTION :{WHITE}... drive through stops can't have junctions
|
||||
STR_ERROR_DRIVE_THROUGH_ON_ONEWAY_ROAD :{WHITE}... road is one way or blocked
|
||||
|
||||
# Station destruction related errors
|
||||
STR_ERROR_CAN_T_REMOVE_PART_OF_STATION :{WHITE}Can't remove part of station...
|
||||
@@ -5262,6 +5262,8 @@ STR_BASESOUNDS_DOS_DESCRIPTION :Original Transp
|
||||
STR_BASESOUNDS_WIN_DESCRIPTION :Original Transport Tycoon Deluxe Windows edition sounds.
|
||||
STR_BASESOUNDS_NONE_DESCRIPTION :A sound pack without any sounds.
|
||||
STR_BASEMUSIC_WIN_DESCRIPTION :Original Transport Tycoon Deluxe Windows edition music.
|
||||
STR_BASEMUSIC_DOS_DESCRIPTION :Original Transport Tycoon Deluxe DOS edition music.
|
||||
STR_BASEMUSIC_TTO_DESCRIPTION :Original Transport Tycoon (Original/World Editor) DOS edition music.
|
||||
STR_BASEMUSIC_NONE_DESCRIPTION :A music pack without actual music.
|
||||
|
||||
##id 0x2000
|
||||
|
@@ -2938,8 +2938,6 @@ STR_TOWN_POPULATION :{BLACK}World po
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (City)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Population: {ORANGE}{COMMA}{BLACK} Houses: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passengers last month: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Mail last month: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Cargo needed for town growth:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} required
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} required in winter
|
||||
|
@@ -2975,8 +2975,6 @@ STR_TOWN_POPULATION :{BLACK}World po
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (City)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Population: {ORANGE}{COMMA}{BLACK} Houses: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passengers last month: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Mail last month: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Cargo needed for town growth:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} required
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} required in winter
|
||||
|
@@ -2507,8 +2507,6 @@ STR_TOWN_POPULATION :{BLACK}Monda en
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Urbo)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Enloĝantoj: {ORANGE}{COMMA}{BLACK} Domoj: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Pasaĝeroj lastmonate: {ORANGE}{COMMA}{BLACK} maksimume: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Poŝto lastmonate: {ORANGE}{COMMA}{BLACK} maksimume: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Kargo bezonata por kreskigi urbon:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} bezonatas
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} bezonatas en vintro
|
||||
|
@@ -3029,8 +3029,6 @@ STR_TOWN_POPULATION :{BLACK}Maailma
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Linn)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Rahvaarv: {ORANGE}{COMMA}{BLACK} Ehitisi: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Reisijaid eelmisel kuul: {ORANGE}{COMMA}{BLACK} Enim: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Posti eelmisel kuul: {ORANGE}{COMMA}{BLACK} Enim: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Veoseid linna kasvamiseks:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} vajalik
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} on talvel vajalik
|
||||
|
@@ -2641,8 +2641,6 @@ STR_TOWN_POPULATION :{BLACK}Heims f
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Býur)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Fólkatal: {ORANGE}{COMMA}{BLACK} Hús: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Ferðafólk síðsta mánað: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Postur síðsta mánað: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Farmur ið tørvast fyri bygda menning:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} krevst
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} krevst um veturin
|
||||
|
@@ -2975,8 +2975,6 @@ STR_TOWN_POPULATION :{BLACK}Maailman
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (City)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Asukasluku: {ORANGE}{COMMA}{BLACK} Taloja: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Matkustajia viime kuussa: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Postia viime kuussa: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Kaupungin kasvuun tarvittava rahti:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} vaadittu
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} vaaditaan talvella
|
||||
|
@@ -2986,8 +2986,7 @@ STR_TOWN_POPULATION :{BLACK}Populati
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Métropole)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Population{NBSP}: {ORANGE}{COMMA}{BLACK} − Maisons{NBSP}: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passagers le mois dernier{NBSP}: {ORANGE}{COMMA}{BLACK} − max.{NBSP}: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Courrier le mois dernier{NBSP}: {ORANGE}{COMMA}{BLACK} − max.{NBSP}: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_LAST_MONTH_MAX :{BLACK}{CARGO_LIST} le mois dernier{NBSP}: {ORANGE}{COMMA}{BLACK} - max.{NBSP}: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Cargaison nécessaire à la croissance de la ville{NBSP}:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} requis
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} requis en hiver
|
||||
|
@@ -3216,8 +3216,6 @@ STR_TOWN_POPULATION :{BLACK}Sluagh a
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Mòr-bhaile)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Sluagh: {ORANGE}{COMMA}{BLACK} Taighean: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Taistealaich am mìos mu dheireadh: {ORANGE}{COMMA}{BLACK} as motha: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post am mìos mu dheireadh: {ORANGE}{COMMA}{BLACK} as motha: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Carago a tha a dhìth ach am fàs am baile:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{RED}Feum air {ORANGE}{STRING}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{BLACK}Feum air {ORANGE}{STRING}{BLACK} sa gheamhradh
|
||||
|
@@ -2976,8 +2976,6 @@ STR_TOWN_POPULATION :{BLACK}Poboaci
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Cidade)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Poboación: {ORANGE}{COMMA}{BLACK} Casas: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Pasaxeiros último mes: {ORANGE}{COMMA}{BLACK} máx: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Correo último mes: {ORANGE}{COMMA}{BLACK} máx: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Carga necesaria para o crecemento da cidade:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} necesario
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} necesarios en inverno
|
||||
|
@@ -3431,8 +3431,6 @@ STR_TOWN_POPULATION :{BLACK}Weltbev
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Großstadt)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Einwohner: {ORANGE}{COMMA}{BLACK} Häuser: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passagiere im letzten Monat: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Postsäcke im letzten Monat: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Für Stadtwachstum benötigte Fracht:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} benötigt
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} im Winter benötigt
|
||||
|
@@ -3090,8 +3090,6 @@ STR_TOWN_POPULATION :{BLACK}Παγκ
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Πόλη)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Πληθυσμός: {ORANGE}{COMMA}{BLACK} Σπίτια: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Επιβάτες τον προηγούμενο μήνα: {ORANGE}{COMMA}{BLACK} μεγ: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Αλληλογραφία τον προηγούμενο μήνα: {ORANGE}{COMMA}{BLACK} μεγ: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Εμπορεύματα που χρειάζονται για την επέκταση της πόλης:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} απαιτείται
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} απαιτείται τον χειμώνα
|
||||
|
@@ -2989,8 +2989,6 @@ STR_TOWN_POPULATION :{BLACK}אוכל
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (עיר)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{ORANGE}{1:COMMA}{BLACK} :בתים {ORANGE}{0:COMMA}{BLACK} :אוכלוסיה
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{ORANGE}{1:COMMA}{BLACK} :מספר מירבי {ORANGE}{0:COMMA}{BLACK} :נוסעים בחודש שעבר
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{ORANGE}{1:COMMA}{BLACK} :מספר מירבי {ORANGE}{0:COMMA}{BLACK} :שקי דואר בחודש שעבר
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}המטען שצריך בשביל גידול עיר
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} דרוש
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} דרוש בחורף
|
||||
|
@@ -3039,8 +3039,6 @@ STR_TOWN_POPULATION :{BLACK}Világn
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Város)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Lakosság: {ORANGE}{COMMA}{BLACK} Házak: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Utasok az előző hónapban: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Levélcsomagok az előző hónapban: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}A város növekedéséhez szükséges rakomány:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} szükséges
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} szükséges télen
|
||||
|
@@ -2799,8 +2799,6 @@ STR_TOWN_POPULATION :{BLACK}Heildar
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Borg)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Íbúafjöldi: {ORANGE}{COMMA}{BLACK} Hús: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Farþegar síðasta mánaðar: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Póstur síðasta mánaðar: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Nauðsynlegur farmur fyrir stækkun bæjarins:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} nauðsynlegt
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} nauðsynlegt á veturnar
|
||||
|
@@ -2969,8 +2969,6 @@ STR_TOWN_POPULATION :{BLACK}Populasi
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (City)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Populasi: {ORANGE}{COMMA}{BLACK} Rumah: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Penumpang bulan lalu: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Surat bulan lalu: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Kargo untuk pertumbuhan kota:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{RED} Butuh {ORANGE}{STRING}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} dibutuhkan saat musim dingin
|
||||
|
@@ -2971,8 +2971,6 @@ STR_TOWN_POPULATION :{BLACK}Daonra d
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Cathair)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Daonra: {ORANGE}{COMMA}{BLACK} Tithe: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Paisinéirí an mhí seo caite: {ORANGE}{COMMA}{BLACK} Uasmhéid: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post an mhí seo caite: {ORANGE}{COMMA}{BLACK} uasmhéid: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Lastas atá ag teastáil le go bhfásfaidh an baile:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} ag teastáil
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} ag teastáil sa Gheimhreadh
|
||||
|
@@ -3015,8 +3015,7 @@ STR_TOWN_POPULATION :{BLACK}Popolazi
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Metropoli)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Popolazione: {ORANGE}{COMMA}{BLACK} Case: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passeggeri il mese scorso: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Posta il mese scorso: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_LAST_MONTH_MAX :{BLACK}{CARGO_LIST} il mese scorso: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Carichi richiesti per la crescita della città:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} richiest{G 0 o o a}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} richiest{G 0 o o a} in inverno
|
||||
|
@@ -2972,8 +2972,6 @@ STR_TOWN_POPULATION :{BLACK}地域
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN}(市)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}人口: {ORANGE}{COMMA}人{BLACK} 建物: {ORANGE}{COMMA}戸
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}旅客数(先月): {ORANGE}{COMMA}人{BLACK} 最大: {ORANGE}{COMMA}人
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}郵便袋(先月): {ORANGE}{COMMA}袋{BLACK} 最大: {ORANGE}{COMMA}袋
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}街の成長に必要な物資:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}が{RED}必要です
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}が{BLACK}冬に必要です
|
||||
|
@@ -3438,8 +3438,6 @@ STR_TOWN_POPULATION :{BLACK}총 인
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (대도시)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}인구: {ORANGE}{COMMA}{BLACK} 가구수: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}지난 달 승객 수: {ORANGE}{COMMA}{BLACK} 최대: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}지난달 우편수: {ORANGE}{COMMA}{BLACK} 최고: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}도시가 성장하기 위해 필요한 화물:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED}{G 0 "이" "가"} 필요함
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :겨울에는 {ORANGE}{STRING}{BLACK}{G 0 "이" "가"} 필요함
|
||||
|
@@ -3177,8 +3177,6 @@ STR_TOWN_POPULATION :{BLACK}Incolae
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Urbs)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Incolae: {ORANGE}{COMMA}{BLACK} Aedificia: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Vectores mensis prioris: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Epistulae mensis prioris: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Onera mandata ad oppidum crescendum:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} mandatur
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} hieme mandatur
|
||||
|
@@ -2906,8 +2906,6 @@ STR_TOWN_POPULATION :{BLACK}Pasaules
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (lielpilsēta)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Iedzīvotāji: {ORANGE}{COMMA}{BLACK} Mājas: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Pasažieri pagājušajā mēnesī: {ORANGE}{COMMA}{BLACK} maksimāli: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Pasts pagājušajā mēnesī: {ORANGE}{COMMA}{BLACK} maksimāli: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Krava nepieciešama pilsētas attīstībai:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} nepieciešams
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} nepieciešams ziemā
|
||||
|
@@ -3190,8 +3190,6 @@ STR_TOWN_POPULATION :{BLACK}Pasaulio
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Miestas)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Populiacija: {ORANGE}{COMMA}{BLACK} Namų skaičius: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Praėjusio mėnesio keleivių sk.: {ORANGE}{COMMA}{BLACK} Daugiausia: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Praėjusio mėnesio pašto siuntų sk.: {ORANGE}{COMMA}{BLACK} Daugiausia: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Kad miestas augtų reikalingi kroviniai:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} reikia
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} reikalingas žiemą
|
||||
|
@@ -2975,8 +2975,6 @@ STR_TOWN_POPULATION :{BLACK}Weltbev
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Stad)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Awunner: {ORANGE}{COMMA}{BLACK} Haiser: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passagéier leschte Mount: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post leschte Mount: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Gidder gebraucht fir Stadwuesstem:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} gebraucht
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} gebraucht am Wanter
|
||||
|
@@ -2679,8 +2679,6 @@ STR_TOWN_POPULATION :{BLACK}Jumlah p
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Bandaraya)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Jumlah penduduk: {ORANGE}{COMMA}{BLACK} Rumah: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Penumpang bulan lalu: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Beg surat bulan lepas: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Kargi yang diperlukan untuk pembesaran bandar:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} diperlukan
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} diperlukan sewaktu musim sejuk
|
||||
|
@@ -2979,8 +2979,6 @@ STR_TOWN_POPULATION :{BLACK}Verdensb
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (By)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Innbyggertall: {ORANGE}{COMMA}{BLACK} Antall hus: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passasjerer forrige måned: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post forrige måned: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Varebehov for byvekst:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} påkrevd
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} nødvendig om vinteren
|
||||
|
@@ -2890,8 +2890,6 @@ STR_TOWN_POPULATION :{BLACK}Verdsinn
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (By)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Innbyggjartal: {ORANGE}{COMMA}{BLACK} Antal hus: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passasjerar førre månad: {ORANGE}{COMMA}{BLACK} maks.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post førre månad: {ORANGE}{COMMA}{BLACK} maks.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Varer naudsynt for folketalsauke:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} naudsynt
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} naudsynt om vinteren
|
||||
|
@@ -3360,8 +3360,6 @@ STR_TOWN_POPULATION :{BLACK}Populacj
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Miasto)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Populacja: {ORANGE}{COMMA}{BLACK} Domów: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Pasażerów w zeszłym miesiącu: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Poczta w zeszłym miesiącu: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Towar potrzebny do rozwoju miasta:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{RED}Wymagana {ORANGE}{STRING}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} wymagane zimą
|
||||
|
@@ -2976,8 +2976,6 @@ STR_TOWN_POPULATION :{BLACK}Populaç
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Metrópole)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}População: {ORANGE}{COMMA}{BLACK} Casas: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passageiros no último mês: {ORANGE}{COMMA}{BLACK} máx: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Correio no último mês: {ORANGE}{COMMA}{BLACK} máx: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Mercadoria necessária para o seu desenvolvimento:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{RED}É necessário {ORANGE}{STRING}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{BLACK}No inverno, é necessário {ORANGE}{STRING}
|
||||
|
@@ -2930,8 +2930,6 @@ STR_TOWN_POPULATION :{BLACK}Populaţ
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Metropolă)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Populaţia: {ORANGE}{COMMA}{BLACK} Locuinţe: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Călători luna trecută: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Colete poştale luna trecută: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Transporturi necesare dezvoltării oraşului:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} necesare
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} necesare iarna
|
||||
|
@@ -3171,8 +3171,6 @@ STR_TOWN_POPULATION :{BLACK}Насе
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Мегаполис)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Население: {ORANGE}{COMMA}{BLACK} Зданий: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Пассажиров в прошлом месяце: {ORANGE}{COMMA}{BLACK}; макс.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Почты в прошлом месяце: {ORANGE}{COMMA}{BLACK}; макс.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Груз, необходимый для роста города:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} требу{G 0 е е е ю}тся
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} требу{G 0 е е е ю}тся зимой
|
||||
|
@@ -3166,8 +3166,6 @@ STR_TOWN_POPULATION :{BLACK}Svetska
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Grad)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Populacija: {ORANGE}{COMMA}{BLACK} Zgrada: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Broj putnika tokom meseca: {ORANGE}{COMMA}{BLACK} najviše: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Količina pošte tokom meseca: {ORANGE}{COMMA}{BLACK} najviše: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Tovar potreban za razvoj naselja:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} potrebno
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} potrebno zimi
|
||||
|
@@ -2972,8 +2972,6 @@ STR_TOWN_POPULATION :{BLACK}所有
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (都市)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}人口:{ORANGE}{COMMA}{BLACK} 房屋:{ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}上月旅客数量:{ORANGE}{COMMA}{BLACK} 最大值:{ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}上月邮包数量:{ORANGE}{COMMA}{BLACK} 最大值:{ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}城镇发展所必需的货物:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{RED} 需要: {ORANGE}{STRING}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} 冬季的需求
|
||||
|
@@ -3039,8 +3039,6 @@ STR_TOWN_POPULATION :{BLACK}Svetová
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Mesto)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Obyvateľstvo: {ORANGE}{COMMA}{BLACK} Domov: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Cestujúci posledný mesiac: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Pošta posledný mesiac: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Tovar potrebný k rozrastu mesta:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} potrebný
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} potrebné v zime
|
||||
|
@@ -3125,8 +3125,6 @@ STR_TOWN_POPULATION :{BLACK}Svetovno
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Mesto)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Prebivalstvo: {ORANGE}{COMMA}{BLACK} Število stavb: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Potnikov prejšnji mesec: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Pošte prejšnji mesec: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Količina potrebnega tovora za rast mesta:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} potrebno
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} je potreben pozimi
|
||||
|
@@ -2976,8 +2976,6 @@ STR_TOWN_POPULATION :{BLACK}Poblaci
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Ciudad)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Habitantes: {ORANGE}{COMMA}{BLACK} Casas: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Pasajeros último mes: {ORANGE}{COMMA}{BLACK} Máx.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Correo último mes: {ORANGE}{COMMA}{BLACK} Máx.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Carga necesaria para crecimiento del municipio:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} requeridos
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} requerido en invierno
|
||||
|
@@ -2986,8 +2986,6 @@ STR_TOWN_POPULATION :{BLACK}Poblaci
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (ciudad)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Habitantes: {ORANGE}{COMMA}{BLACK} Casas: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Pasajeros mes pasado: {ORANGE}{COMMA}{BLACK} Máx.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Correo mes pasado: {ORANGE}{COMMA}{BLACK} Máx.: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Cargamento necesario para crecimiento:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} requeridos
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} requerido en invierno
|
||||
|
@@ -2975,8 +2975,6 @@ STR_TOWN_POPULATION :{BLACK}Global f
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Stad)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Invånare: {ORANGE}{COMMA}{BLACK} Hus: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passagerare förra månaden: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post förra månaden: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Fraktgods behövs för ortens tillväxt:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} krävs
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} krävs under vintern
|
||||
|
@@ -2618,8 +2618,6 @@ STR_TOWN_POPULATION :{BLACK}உல
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (மாநகரம்)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}மக்கள்தொகை: {ORANGE}{COMMA}{BLACK} வீடுகள்: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}சென்ற மாத பயணிகள்: {ORANGE}{COMMA}{BLACK} அதிகம்: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}சென்ற மாத அஞ்சல்கள்: {ORANGE}{COMMA}{BLACK} அதிகம்: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}நகரத்தின் வளர்ச்சியிற்கு தேவையான சரக்குகள்:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} தேவை
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} குளிர்காலத்தில் தேவை
|
||||
|
@@ -2903,8 +2903,6 @@ STR_TOWN_POPULATION :{BLACK}ปร
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}นคร {TOWN}
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}จำนวนประชากร: {ORANGE}{COMMA}{BLACK} จำนวนอาคาร: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}ผู้โดยสารเมื่อเดือนที่แล้ว: {ORANGE}{COMMA}{BLACK} จำนวนสูงสุด: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}ไปรษณีย์ภัณฑ์เมื่อเดือนที่แล้ว: {ORANGE}{COMMA}{BLACK} จำนวนสูงสุด: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}ความต้องการสินค้าสำหรับการขยายตัวของเมือง:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} required
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} ต้องการหิมะ
|
||||
|
@@ -2971,8 +2971,6 @@ STR_TOWN_POPULATION :{BLACK}世界
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (城市)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}人口:{ORANGE}{COMMA}{BLACK} 房屋:{ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}上月乘客數字:{ORANGE}{COMMA}{BLACK} 紀錄最高:{ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}上月郵件數量:{ORANGE}{COMMA}{BLACK} 紀錄最高:{ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}市鎮成長所需貨物:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{RED}需要 {ORANGE}{STRING}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} 必須是冬天
|
||||
|
@@ -2976,8 +2976,6 @@ STR_TOWN_POPULATION :{BLACK}Dünya n
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Şehir)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Nüfus: {ORANGE}{COMMA}{BLACK} Ev: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Geçen ayki yolcu: {ORANGE}{COMMA}{BLACK} azami: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Geçen ayki posta: {ORANGE}{COMMA}{BLACK} azami: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Kasaba büyümesi için gerekli kargo:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} gerekli
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} kışın gerekir
|
||||
|
@@ -3102,8 +3102,6 @@ STR_TOWN_POPULATION :{BLACK}Насе
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (місто)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Населення: {ORANGE}{COMMA}{BLACK} Будинки: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Пасажирів за місяць: {ORANGE}{COMMA}{BLACK} найбільше: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Пошти за місяць: {ORANGE}{COMMA}{BLACK} найбільше: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Вантаж, потрібний для зростання міста:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} потрібно
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} потрібно взимку
|
||||
|
@@ -2747,8 +2747,6 @@ STR_TOWN_POPULATION :{BLACK}Wrâldpo
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (City)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Ynwenners: {ORANGE}{COMMA}{BLACK} Hûzen: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Passazjiers lêste moanne: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post lêste moanne: {ORANGE}{COMMA}{BLACK} maks: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Guod nedich foar stêdsgroei:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} ferplichte
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} ferplichte yn de winter
|
||||
|
@@ -2656,8 +2656,6 @@ STR_TOWN_POPULATION :{BLACK}جمعی
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (شهر)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}جمعیت: {ORANGE}{COMMA}{BLACK} خانه ها: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}مسافران در ماه گذشته: {ORANGE}{COMMA}{BLACK} حداکثر: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}نامه ها در ماه گذشته: {ORANGE}{COMMA}{BLACK} حداکثر: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}نوع بار برای رشد شهر:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} ضروری
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} در زمستان ضروری است
|
||||
|
@@ -2975,8 +2975,6 @@ STR_TOWN_POPULATION :{BLACK}Dân s
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Thành Phố)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Dân số: {ORANGE}{COMMA}{BLACK} Toà nhà: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Du khách tháng trước: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Bưu kiện tháng trước: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Hàng hoá cần để đô thị tăng trưởng:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{ORANGE}{STRING}{RED} được yêu cầu
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} Yêu cầu trong mùa đông
|
||||
|
@@ -2975,8 +2975,6 @@ STR_TOWN_POPULATION :{BLACK}Poblogae
|
||||
STR_TOWN_VIEW_TOWN_CAPTION :{WHITE}{TOWN}
|
||||
STR_TOWN_VIEW_CITY_CAPTION :{WHITE}{TOWN} (Dinas)
|
||||
STR_TOWN_VIEW_POPULATION_HOUSES :{BLACK}Poblogaeth: {ORANGE}{COMMA}{BLACK} Tai: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX :{BLACK}Teithwyr mis diwethaf: {ORANGE}{COMMA}{BLACK} uchafswm: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX :{BLACK}Post mis diwethaf: {ORANGE}{COMMA}{BLACK} uchafswm: {ORANGE}{COMMA}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH :{BLACK}Nwyddau angenrheidiol ar gyfer tyfiant y dref:
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_GENERAL :{RED}Angen {ORANGE}{STRING}
|
||||
STR_TOWN_VIEW_CARGO_FOR_TOWNGROWTH_REQUIRED_WINTER :{ORANGE}{STRING}{BLACK} ei angen yn y gaeaf
|
||||
|
@@ -323,10 +323,7 @@ public:
|
||||
for (CargoID i = 0; i < NUM_CARGO; ++i) {
|
||||
if (acceptance[i] > 0) {
|
||||
/* Add a comma between each item. */
|
||||
if (found) {
|
||||
*strp++ = ',';
|
||||
*strp++ = ' ';
|
||||
}
|
||||
if (found) strp = strecpy(strp, ", ", lastof(this->landinfo_data[LAND_INFO_MULTICENTER_LINE]));
|
||||
found = true;
|
||||
|
||||
/* If the accepted value is less than 8, show it in 1/8:ths */
|
||||
|
@@ -11,11 +11,69 @@
|
||||
|
||||
#include "stdafx.h"
|
||||
|
||||
|
||||
/** The type of set we're replacing */
|
||||
#define SET_TYPE "music"
|
||||
#include "base_media_func.h"
|
||||
|
||||
#include "safeguards.h"
|
||||
#include "fios.h"
|
||||
|
||||
|
||||
/**
|
||||
* Read the name of a music CAT file entry.
|
||||
* @param filename Name of CAT file to read from
|
||||
* @param entrynum Index of entry whose name to read
|
||||
* @return Pointer to string, caller is responsible for freeing memory,
|
||||
* NULL if entrynum does not exist.
|
||||
*/
|
||||
char *GetMusicCatEntryName(const char *filename, size_t entrynum)
|
||||
{
|
||||
if (!FioCheckFileExists(filename, BASESET_DIR)) return NULL;
|
||||
|
||||
FioOpenFile(CONFIG_SLOT, filename, BASESET_DIR);
|
||||
uint32 ofs = FioReadDword();
|
||||
size_t entry_count = ofs / 8;
|
||||
if (entrynum < entry_count) {
|
||||
FioSeekTo(entrynum * 8, SEEK_SET);
|
||||
FioSeekTo(FioReadDword(), SEEK_SET);
|
||||
byte namelen = FioReadByte();
|
||||
char *name = MallocT<char>(namelen + 1);
|
||||
FioReadBlock(name, namelen);
|
||||
name[namelen] = '\0';
|
||||
return name;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the full data of a music CAT file entry.
|
||||
* @param filename Name of CAT file to read from.
|
||||
* @param entrynum Index of entry to read
|
||||
* @param[out] entrylen Receives length of data read
|
||||
* @return Pointer to buffer with data read, caller is responsible for freeind memory,
|
||||
* NULL if entrynum does not exist.
|
||||
*/
|
||||
byte *GetMusicCatEntryData(const char *filename, size_t entrynum, size_t &entrylen)
|
||||
{
|
||||
entrylen = 0;
|
||||
if (!FioCheckFileExists(filename, BASESET_DIR)) return NULL;
|
||||
|
||||
FioOpenFile(CONFIG_SLOT, filename, BASESET_DIR);
|
||||
uint32 ofs = FioReadDword();
|
||||
size_t entry_count = ofs / 8;
|
||||
if (entrynum < entry_count) {
|
||||
FioSeekTo(entrynum * 8, SEEK_SET);
|
||||
size_t entrypos = FioReadDword();
|
||||
entrylen = FioReadDword();
|
||||
FioSeekTo(entrypos, SEEK_SET);
|
||||
FioSkipBytes(FioReadByte());
|
||||
byte *data = MallocT<byte>(entrylen);
|
||||
FioReadBlock(data, entrylen);
|
||||
return data;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
INSTANTIATE_BASE_MEDIA_METHODS(BaseMedia<MusicSet>, MusicSet)
|
||||
|
||||
@@ -66,14 +124,32 @@ bool MusicSet::FillSetDetails(IniFile *ini, const char *path, const char *full_f
|
||||
if (ret) {
|
||||
this->num_available = 0;
|
||||
IniGroup *names = ini->GetGroup("names");
|
||||
for (uint i = 0, j = 1; i < lengthof(this->song_name); i++) {
|
||||
IniGroup *catindex = ini->GetGroup("catindex");
|
||||
for (uint i = 0, j = 1; i < lengthof(this->songinfo); i++) {
|
||||
const char *filename = this->files[i].filename;
|
||||
if (names == NULL || StrEmpty(filename)) {
|
||||
this->song_name[i][0] = '\0';
|
||||
this->songinfo[i].songname[0] = '\0';
|
||||
continue;
|
||||
}
|
||||
|
||||
IniItem *item = NULL;
|
||||
this->songinfo[i].filename = filename; // non-owned pointer
|
||||
|
||||
IniItem *item = catindex->GetItem(_music_file_names[i], false);
|
||||
if (item != NULL && !StrEmpty(item->value)) {
|
||||
/* Song has a CAT file index, assume it's MPS MIDI format */
|
||||
this->songinfo[i].filetype = MTT_MPSMIDI;
|
||||
this->songinfo[i].cat_index = atoi(item->value);
|
||||
char *songname = GetMusicCatEntryName(filename, this->songinfo[i].cat_index);
|
||||
if (songname == NULL) {
|
||||
DEBUG(grf, 0, "Base music set song missing from CAT file: %s/%d", filename, this->songinfo[i].cat_index);
|
||||
return false;
|
||||
}
|
||||
strecpy(this->songinfo[i].songname, songname, lastof(this->songinfo[i].songname));
|
||||
free(songname);
|
||||
} else {
|
||||
this->songinfo[i].filetype = MTT_STANDARDMIDI;
|
||||
}
|
||||
|
||||
/* As we possibly add a path to the filename and we compare
|
||||
* on the filename with the path as in the .obm, we need to
|
||||
* keep stripping path elements until we find a match. */
|
||||
@@ -86,14 +162,17 @@ bool MusicSet::FillSetDetails(IniFile *ini, const char *path, const char *full_f
|
||||
if (item != NULL && !StrEmpty(item->value)) break;
|
||||
}
|
||||
|
||||
if (item == NULL || StrEmpty(item->value)) {
|
||||
DEBUG(grf, 0, "Base music set song name missing: %s", filename);
|
||||
return false;
|
||||
if (this->songinfo[i].filetype == MTT_STANDARDMIDI) {
|
||||
if (item != NULL && !StrEmpty(item->value)) {
|
||||
strecpy(this->songinfo[i].songname, item->value, lastof(this->songinfo[i].songname));
|
||||
} else {
|
||||
DEBUG(grf, 0, "Base music set song name missing: %s", filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
strecpy(this->song_name[i], item->value, lastof(this->song_name[i]));
|
||||
this->track_nr[i] = j++;
|
||||
this->num_available++;
|
||||
|
||||
this->songinfo[i].tracknr = j++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
|
@@ -14,6 +14,7 @@
|
||||
#include "../stdafx.h"
|
||||
#include "../debug.h"
|
||||
#include "allegro_m.h"
|
||||
#include "midifile.hpp"
|
||||
#include <allegro.h>
|
||||
|
||||
#include "../safeguards.h"
|
||||
@@ -58,11 +59,17 @@ void MusicDriver_Allegro::Stop()
|
||||
if (--_allegro_instance_count == 0) allegro_exit();
|
||||
}
|
||||
|
||||
void MusicDriver_Allegro::PlaySong(const char *filename)
|
||||
void MusicDriver_Allegro::PlaySong(const MusicSongInfo &song)
|
||||
{
|
||||
std::string filename = MidiFile::GetSMFFile(song);
|
||||
|
||||
if (_midi != NULL) destroy_midi(_midi);
|
||||
_midi = load_midi(filename);
|
||||
play_midi(_midi, false);
|
||||
if (!filename.empty()) {
|
||||
_midi = load_midi(filename.c_str());
|
||||
play_midi(_midi, false);
|
||||
} else {
|
||||
_midi = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void MusicDriver_Allegro::StopSong()
|
||||
|
@@ -21,7 +21,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop();
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename);
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song);
|
||||
|
||||
/* virtual */ void StopSong();
|
||||
|
||||
|
@@ -12,6 +12,8 @@
|
||||
#include "../stdafx.h"
|
||||
#include "../openttd.h"
|
||||
#include "bemidi.h"
|
||||
#include "../base_media_base.h"
|
||||
#include "midifile.hpp"
|
||||
|
||||
/* BeOS System Includes */
|
||||
#include <MidiSynthFile.h>
|
||||
@@ -34,13 +36,17 @@ void MusicDriver_BeMidi::Stop()
|
||||
midiSynthFile.UnloadFile();
|
||||
}
|
||||
|
||||
void MusicDriver_BeMidi::PlaySong(const char *filename)
|
||||
void MusicDriver_BeMidi::PlaySong(const MusicSongInfo &song)
|
||||
{
|
||||
std::string filename = MidiFile::GetSMFFile(song);
|
||||
|
||||
this->Stop();
|
||||
entry_ref midiRef;
|
||||
get_ref_for_path(filename, &midiRef);
|
||||
midiSynthFile.LoadFile(&midiRef);
|
||||
midiSynthFile.Start();
|
||||
if (!filename.empty()) {
|
||||
entry_ref midiRef;
|
||||
get_ref_for_path(filename.c_str(), &midiRef);
|
||||
midiSynthFile.LoadFile(&midiRef);
|
||||
midiSynthFile.Start();
|
||||
}
|
||||
}
|
||||
|
||||
void MusicDriver_BeMidi::StopSong()
|
||||
|
@@ -21,7 +21,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop();
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename);
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song);
|
||||
|
||||
/* virtual */ void StopSong();
|
||||
|
||||
|
@@ -18,7 +18,9 @@
|
||||
#include "../stdafx.h"
|
||||
#include "../os/macosx/macos.h"
|
||||
#include "cocoa_m.h"
|
||||
#include "midifile.hpp"
|
||||
#include "../debug.h"
|
||||
#include "../base_media_base.h"
|
||||
|
||||
#define Rect OTTDRect
|
||||
#define Point OTTDPoint
|
||||
@@ -141,11 +143,13 @@ void MusicDriver_Cocoa::Stop()
|
||||
/**
|
||||
* Starts playing a new song.
|
||||
*
|
||||
* @param filename Path to a MIDI file.
|
||||
* @param song Description of music to load and play
|
||||
*/
|
||||
void MusicDriver_Cocoa::PlaySong(const char *filename)
|
||||
void MusicDriver_Cocoa::PlaySong(const MusicSongInfo &song)
|
||||
{
|
||||
DEBUG(driver, 2, "cocoa_m: trying to play '%s'", filename);
|
||||
std::string filename = MidiFile::GetSMFFile(song);
|
||||
|
||||
DEBUG(driver, 2, "cocoa_m: trying to play '%s'", filename.c_str());
|
||||
|
||||
this->StopSong();
|
||||
if (_sequence != NULL) {
|
||||
@@ -153,12 +157,14 @@ void MusicDriver_Cocoa::PlaySong(const char *filename)
|
||||
_sequence = NULL;
|
||||
}
|
||||
|
||||
if (filename.empty()) return;
|
||||
|
||||
if (NewMusicSequence(&_sequence) != noErr) {
|
||||
DEBUG(driver, 0, "cocoa_m: Failed to create music sequence");
|
||||
return;
|
||||
}
|
||||
|
||||
const char *os_file = OTTD2FS(filename);
|
||||
const char *os_file = OTTD2FS(filename.c_str());
|
||||
CFURLRef url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, (const UInt8*)os_file, strlen(os_file), false);
|
||||
|
||||
#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
|
||||
@@ -218,7 +224,7 @@ void MusicDriver_Cocoa::PlaySong(const char *filename)
|
||||
if (MusicPlayerStart(_player) != noErr) return;
|
||||
_playing = true;
|
||||
|
||||
DEBUG(driver, 3, "cocoa_m: playing '%s'", filename);
|
||||
DEBUG(driver, 3, "cocoa_m: playing '%s'", filename.c_str());
|
||||
}
|
||||
|
||||
|
||||
|
@@ -20,7 +20,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop();
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename);
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song);
|
||||
|
||||
/* virtual */ void StopSong();
|
||||
|
||||
|
@@ -21,6 +21,7 @@
|
||||
#include "../core/mem_func.hpp"
|
||||
#include "../thread/thread.h"
|
||||
#include "../fileio_func.h"
|
||||
#include "../base_media_base.h"
|
||||
#include "dmusic.h"
|
||||
#include "midifile.hpp"
|
||||
#include "midi.h"
|
||||
@@ -1225,11 +1226,12 @@ void MusicDriver_DMusic::Stop()
|
||||
}
|
||||
|
||||
|
||||
void MusicDriver_DMusic::PlaySong(const char *filename)
|
||||
void MusicDriver_DMusic::PlaySong(const MusicSongInfo &song)
|
||||
{
|
||||
ThreadMutexLocker lock(_thread_mutex);
|
||||
|
||||
_playback.next_file.LoadFile(filename);
|
||||
if (!_playback.next_file.LoadSong(song)) return;
|
||||
|
||||
_playback.next_segment.start = 0;
|
||||
_playback.next_segment.end = 0;
|
||||
_playback.next_segment.loop = false;
|
||||
|
@@ -23,7 +23,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop();
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename);
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song);
|
||||
|
||||
/* virtual */ void StopSong();
|
||||
|
||||
|
@@ -17,6 +17,8 @@
|
||||
#include "../video/video_driver.hpp"
|
||||
#include "../gfx_func.h"
|
||||
#include "extmidi.h"
|
||||
#include "../base_media_base.h"
|
||||
#include "midifile.hpp"
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
@@ -83,10 +85,13 @@ void MusicDriver_ExtMidi::Stop()
|
||||
this->DoStop();
|
||||
}
|
||||
|
||||
void MusicDriver_ExtMidi::PlaySong(const char *filename)
|
||||
void MusicDriver_ExtMidi::PlaySong(const MusicSongInfo &song)
|
||||
{
|
||||
strecpy(this->song, filename, lastof(this->song));
|
||||
this->DoStop();
|
||||
std::string filename = MidiFile::GetSMFFile(song);
|
||||
if (!filename.empty()) {
|
||||
strecpy(this->song, filename.c_str(), lastof(this->song));
|
||||
this->DoStop();
|
||||
}
|
||||
}
|
||||
|
||||
void MusicDriver_ExtMidi::StopSong()
|
||||
|
@@ -28,7 +28,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop();
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename);
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song);
|
||||
|
||||
/* virtual */ void StopSong();
|
||||
|
||||
|
@@ -14,6 +14,8 @@
|
||||
#include "../sound_type.h"
|
||||
#include "../debug.h"
|
||||
#include "libtimidity.h"
|
||||
#include "midifile.hpp"
|
||||
#include "../base_media_base.h"
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
@@ -73,11 +75,14 @@ void MusicDriver_LibTimidity::Stop()
|
||||
mid_exit();
|
||||
}
|
||||
|
||||
void MusicDriver_LibTimidity::PlaySong(const char *filename)
|
||||
void MusicDriver_LibTimidity::PlaySong(const MusicSongInfo &song)
|
||||
{
|
||||
this->StopSong();
|
||||
std::string filename = MidiFile::GetSMFFile(song);
|
||||
|
||||
_midi.stream = mid_istream_open_file(filename);
|
||||
this->StopSong();
|
||||
if (filename.empty()) return;
|
||||
|
||||
_midi.stream = mid_istream_open_file(filename.c_str());
|
||||
if (_midi.stream == NULL) {
|
||||
DEBUG(driver, 0, "Could not open music file");
|
||||
return;
|
||||
|
@@ -21,7 +21,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop();
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename);
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song);
|
||||
|
||||
/* virtual */ void StopSong();
|
||||
|
||||
|
@@ -12,13 +12,20 @@
|
||||
#include "midifile.hpp"
|
||||
#include "../fileio_func.h"
|
||||
#include "../fileio_type.h"
|
||||
#include "../string_func.h"
|
||||
#include "../core/endian_func.hpp"
|
||||
#include "../base_media_base.h"
|
||||
#include "midi.h"
|
||||
#include <algorithm>
|
||||
|
||||
#include "../console_func.h"
|
||||
#include "../console_internal.h"
|
||||
|
||||
/* implementation based on description at: http://www.somascape.org/midi/tech/mfile.html */
|
||||
|
||||
/* SMF reader based on description at: http://www.somascape.org/midi/tech/mfile.html */
|
||||
|
||||
|
||||
static MidiFile *_midifile_instance = NULL;
|
||||
|
||||
/**
|
||||
* Owning byte buffer readable as a stream.
|
||||
@@ -158,7 +165,7 @@ static bool ReadTrackChunk(FILE *file, MidiFile &target)
|
||||
return false;
|
||||
}
|
||||
|
||||
/* read chunk length and then the whole chunk */
|
||||
/* Read chunk length and then the whole chunk */
|
||||
uint32 chunk_length;
|
||||
if (fread(&chunk_length, 1, 4, file) != 4) {
|
||||
return false;
|
||||
@@ -176,7 +183,7 @@ static bool ReadTrackChunk(FILE *file, MidiFile &target)
|
||||
byte last_status = 0;
|
||||
bool running_sysex = false;
|
||||
while (!chunk.IsEnd()) {
|
||||
/* read deltatime for event, start new block */
|
||||
/* Read deltatime for event, start new block */
|
||||
uint32 deltatime = 0;
|
||||
if (!chunk.ReadVariableLength(deltatime)) {
|
||||
return false;
|
||||
@@ -186,14 +193,14 @@ static bool ReadTrackChunk(FILE *file, MidiFile &target)
|
||||
block = &target.blocks.back();
|
||||
}
|
||||
|
||||
/* read status byte */
|
||||
/* Read status byte */
|
||||
byte status;
|
||||
if (!chunk.ReadByte(status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((status & 0x80) == 0) {
|
||||
/* high bit not set means running status message, status is same as last
|
||||
/* High bit not set means running status message, status is same as last
|
||||
* convert to explicit status */
|
||||
chunk.Rewind(1);
|
||||
status = last_status;
|
||||
@@ -266,7 +273,7 @@ static bool ReadTrackChunk(FILE *file, MidiFile &target)
|
||||
return false;
|
||||
}
|
||||
if (data[length] != 0xF7) {
|
||||
/* engage Casio weirdo mode - convert to normal sysex */
|
||||
/* Engage Casio weirdo mode - convert to normal sysex */
|
||||
running_sysex = true;
|
||||
*block->data.Append() = 0xF7;
|
||||
} else {
|
||||
@@ -312,18 +319,20 @@ static bool FixupMidiData(MidiFile &target)
|
||||
std::sort(target.blocks.begin(), target.blocks.end(), TicktimeAscending<MidiFile::DataBlock>);
|
||||
|
||||
if (target.tempos.size() == 0) {
|
||||
/* no tempo information, assume 120 bpm (500,000 microseconds per beat */
|
||||
/* No tempo information, assume 120 bpm (500,000 microseconds per beat */
|
||||
target.tempos.push_back(MidiFile::TempoChange(0, 500000));
|
||||
}
|
||||
/* add sentinel tempo at end */
|
||||
/* Add sentinel tempo at end */
|
||||
target.tempos.push_back(MidiFile::TempoChange(UINT32_MAX, 0));
|
||||
|
||||
/* merge blocks with identical tick times */
|
||||
/* Merge blocks with identical tick times */
|
||||
std::vector<MidiFile::DataBlock> merged_blocks;
|
||||
uint32 last_ticktime = 0;
|
||||
for (size_t i = 0; i < target.blocks.size(); i++) {
|
||||
MidiFile::DataBlock &block = target.blocks[i];
|
||||
if (block.ticktime > last_ticktime || merged_blocks.size() == 0) {
|
||||
if (block.data.Length() == 0) {
|
||||
continue;
|
||||
} else if (block.ticktime > last_ticktime || merged_blocks.size() == 0) {
|
||||
merged_blocks.push_back(block);
|
||||
last_ticktime = block.ticktime;
|
||||
} else {
|
||||
@@ -333,7 +342,7 @@ static bool FixupMidiData(MidiFile &target)
|
||||
}
|
||||
std::swap(merged_blocks, target.blocks);
|
||||
|
||||
/* annotate blocks with real time */
|
||||
/* Annotate blocks with real time */
|
||||
last_ticktime = 0;
|
||||
uint32 last_realtime = 0;
|
||||
size_t cur_tempo = 0, cur_block = 0;
|
||||
@@ -390,13 +399,13 @@ bool MidiFile::ReadSMFHeader(FILE *file, SMFHeader &header)
|
||||
return false;
|
||||
}
|
||||
|
||||
/* check magic, 'MThd' followed by 4 byte length indicator (always = 6 in SMF) */
|
||||
/* Check magic, 'MThd' followed by 4 byte length indicator (always = 6 in SMF) */
|
||||
const byte magic[] = { 'M', 'T', 'h', 'd', 0x00, 0x00, 0x00, 0x06 };
|
||||
if (MemCmpT(buffer, magic, sizeof(magic)) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* read the parameters of the file */
|
||||
/* Read the parameters of the file */
|
||||
header.format = (buffer[8] << 8) | buffer[9];
|
||||
header.tracks = (buffer[10] << 8) | buffer[11];
|
||||
header.tickdiv = (buffer[12] << 8) | buffer[13];
|
||||
@@ -410,12 +419,15 @@ bool MidiFile::ReadSMFHeader(FILE *file, SMFHeader &header)
|
||||
*/
|
||||
bool MidiFile::LoadFile(const char *filename)
|
||||
{
|
||||
_midifile_instance = this;
|
||||
|
||||
this->blocks.clear();
|
||||
this->tempos.clear();
|
||||
this->tickdiv = 0;
|
||||
|
||||
bool success = false;
|
||||
FILE *file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR);
|
||||
if (file == NULL) return false;
|
||||
|
||||
SMFHeader header;
|
||||
if (!ReadSMFHeader(file, header)) goto cleanup;
|
||||
@@ -440,6 +452,383 @@ cleanup:
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decoder for "MPS MIDI" format data.
|
||||
* This format for MIDI music is also used in a few other Microprose games contemporary with Transport Tycoon.
|
||||
*
|
||||
* The song data are usually packed inside a CAT file, with one CAT chunk per song. The song titles are used as names for the CAT chunks.
|
||||
*
|
||||
* Unlike the Standard MIDI File format, which is based on the IFF structure, the MPS MIDI format is best described as two linked lists of sub-tracks,
|
||||
* the first list contains a number of reusable "segments", and the second list contains the "master tracks". Each list is prefixed with a byte
|
||||
* giving the number of elements in the list, and the actual list is just a byte count (BE16 format) for the segment/track followed by the actual data,
|
||||
* there is no index as such, so the entire data must be seeked through to build an index.
|
||||
*
|
||||
* The actual MIDI data inside each track is almost standard MIDI, prefixing every event with a delay, encoded using the same variable-length format
|
||||
* used in SMF. A few status codes have changed meaning in MPS MIDI: 0xFE changes control from master track to a segment, 0xFD returns from a segment
|
||||
* to the master track, and 0xFF is used to end the song. (In Standard MIDI all those values must only occur in real-time data.)
|
||||
*
|
||||
* As implemented in the original decoder, there is no support for recursively calling segments from segments, i.e. code 0xFE must only occur in
|
||||
* a master track, and code 0xFD must only occur in a segment. There are no checks made for this, it's assumed that the only input data will ever
|
||||
* be the original game music, not music from other games, or new productions.
|
||||
*
|
||||
* Additionally, some program change and controller events are given special meaning, see comments in the code.
|
||||
*/
|
||||
struct MpsMachine {
|
||||
/** Starting parameter and playback status for one channel/track */
|
||||
struct Channel {
|
||||
byte cur_program; ///< program selected, used for velocity scaling (lookup into programvelocities array)
|
||||
byte running_status; ///< last midi status code seen
|
||||
uint16 delay; ///< frames until next command
|
||||
uint32 playpos; ///< next byte to play this channel from
|
||||
uint32 startpos; ///< start position of master track
|
||||
uint32 returnpos; ///< next return position after playing a segment
|
||||
Channel() : cur_program(0xFF), running_status(0), delay(0), playpos(0), startpos(0), returnpos(0) { }
|
||||
};
|
||||
Channel channels[16]; ///< playback status for each MIDI channel
|
||||
std::vector<uint32> segments; ///< pointers into songdata to repeatable data segments
|
||||
int16 tempo_ticks; ///< ticker that increments when playing a frame, decrements before playing a frame
|
||||
int16 current_tempo; ///< threshold for actually playing a frame
|
||||
int16 initial_tempo; ///< starting tempo of song
|
||||
bool shouldplayflag; ///< not-end-of-song flag
|
||||
|
||||
static const int TEMPO_RATE;
|
||||
static const byte programvelocities[128];
|
||||
|
||||
const byte *songdata; ///< raw data array
|
||||
size_t songdatalen; ///< length of song data
|
||||
MidiFile ⌖ ///< recipient of data
|
||||
|
||||
/** Overridden MIDI status codes used in the data format */
|
||||
enum MpsMidiStatus {
|
||||
MPSMIDIST_SEGMENT_RETURN = 0xFD, ///< resume playing master track from stored position
|
||||
MPSMIDIST_SEGMENT_CALL = 0xFE, ///< store current position of master track playback, and begin playback of a segment
|
||||
MPSMIDIST_ENDSONG = 0xFF, ///< immediately end the song
|
||||
};
|
||||
|
||||
static void AddMidiData(MidiFile::DataBlock &block, byte b1, byte b2)
|
||||
{
|
||||
*block.data.Append() = b1;
|
||||
*block.data.Append() = b2;
|
||||
}
|
||||
static void AddMidiData(MidiFile::DataBlock &block, byte b1, byte b2, byte b3)
|
||||
{
|
||||
*block.data.Append() = b1;
|
||||
*block.data.Append() = b2;
|
||||
*block.data.Append() = b3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a TTD DOS music format decoder.
|
||||
* @param songdata Buffer of song data from CAT file, ownership remains with caller
|
||||
* @param songdatalen Length of the data buffer in bytes
|
||||
* @param target MidiFile object to add decoded data to
|
||||
*/
|
||||
MpsMachine(const byte *data, size_t length, MidiFile &target)
|
||||
: songdata(data), songdatalen(length), target(target)
|
||||
{
|
||||
uint32 pos = 0;
|
||||
int loopmax;
|
||||
int loopidx;
|
||||
|
||||
/* First byte is the initial "tempo" */
|
||||
this->initial_tempo = this->songdata[pos++];
|
||||
|
||||
/* Next byte is a count of callable segments */
|
||||
loopmax = this->songdata[pos++];
|
||||
for (loopidx = 0; loopidx < loopmax; loopidx++) {
|
||||
/* Segments form a linked list in the stream,
|
||||
* first two bytes in each is an offset to the next.
|
||||
* Two bytes between offset to next and start of data
|
||||
* are unaccounted for. */
|
||||
this->segments.push_back(pos + 4);
|
||||
pos += FROM_LE16(*(const int16 *)(this->songdata + pos));
|
||||
}
|
||||
|
||||
/* After segments follows list of master tracks for each channel,
|
||||
* also prefixed with a byte counting actual tracks. */
|
||||
loopmax = this->songdata[pos++];
|
||||
for (loopidx = 0; loopidx < loopmax; loopidx++) {
|
||||
/* Similar structure to segments list, but also has
|
||||
* the MIDI channel number as a byte before the offset
|
||||
* to next track. */
|
||||
byte ch = this->songdata[pos++];
|
||||
this->channels[ch].startpos = pos + 4;
|
||||
pos += FROM_LE16(*(const int16 *)(this->songdata + pos));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an SMF-style variable length value (note duration) from songdata.
|
||||
* @param pos Position to read from, updated to point to next byte after the value read
|
||||
* @return Value read from data stream
|
||||
*/
|
||||
uint16 ReadVariableLength(uint32 &pos)
|
||||
{
|
||||
byte b = 0;
|
||||
uint16 res = 0;
|
||||
do {
|
||||
b = this->songdata[pos++];
|
||||
res = (res << 7) + (b & 0x7F);
|
||||
} while (b & 0x80);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare for playback from the beginning. Resets the song pointer for every track to the beginning.
|
||||
*/
|
||||
void RestartSong()
|
||||
{
|
||||
for (int ch = 0; ch < 16; ch++) {
|
||||
Channel &chandata = this->channels[ch];
|
||||
if (chandata.startpos != 0) {
|
||||
/* Active track, set position to beginning */
|
||||
chandata.playpos = chandata.startpos;
|
||||
chandata.delay = this->ReadVariableLength(chandata.playpos);
|
||||
} else {
|
||||
/* Inactive track, mark as such */
|
||||
chandata.playpos = 0;
|
||||
chandata.delay = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play one frame of data from one channel
|
||||
*/
|
||||
uint16 PlayChannelFrame(MidiFile::DataBlock &outblock, int channel)
|
||||
{
|
||||
uint16 newdelay = 0;
|
||||
byte b1, b2;
|
||||
Channel &chandata = this->channels[channel];
|
||||
|
||||
do {
|
||||
/* Read command/status byte */
|
||||
b1 = this->songdata[chandata.playpos++];
|
||||
|
||||
/* Command 0xFE, call segment from master track */
|
||||
if (b1 == MPSMIDIST_SEGMENT_CALL) {
|
||||
b1 = this->songdata[chandata.playpos++];
|
||||
chandata.returnpos = chandata.playpos;
|
||||
chandata.playpos = this->segments[b1];
|
||||
newdelay = this->ReadVariableLength(chandata.playpos);
|
||||
if (newdelay == 0) {
|
||||
continue;
|
||||
}
|
||||
return newdelay;
|
||||
}
|
||||
|
||||
/* Command 0xFD, return from segment to master track */
|
||||
if (b1 == MPSMIDIST_SEGMENT_RETURN) {
|
||||
chandata.playpos = chandata.returnpos;
|
||||
chandata.returnpos = 0;
|
||||
newdelay = this->ReadVariableLength(chandata.playpos);
|
||||
if (newdelay == 0) {
|
||||
continue;
|
||||
}
|
||||
return newdelay;
|
||||
}
|
||||
|
||||
/* Command 0xFF, end of song */
|
||||
if (b1 == MPSMIDIST_ENDSONG) {
|
||||
this->shouldplayflag = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Regular MIDI channel message status byte */
|
||||
if (b1 >= 0x80) {
|
||||
/* Save the status byte as running status for the channel
|
||||
* and read another byte for first parameter to command */
|
||||
chandata.running_status = b1;
|
||||
b1 = this->songdata[chandata.playpos++];
|
||||
}
|
||||
|
||||
switch (chandata.running_status & 0xF0) {
|
||||
case MIDIST_NOTEOFF:
|
||||
case MIDIST_NOTEON:
|
||||
b2 = this->songdata[chandata.playpos++];
|
||||
if (b2 != 0) {
|
||||
/* Note on, read velocity and scale according to rules */
|
||||
int16 velocity;
|
||||
if (channel == 9) {
|
||||
/* Percussion channel, fixed velocity scaling not in the table */
|
||||
velocity = (int16)b2 * 0x50;
|
||||
} else {
|
||||
/* Regular channel, use scaling from table */
|
||||
velocity = b2 * programvelocities[chandata.cur_program];
|
||||
}
|
||||
b2 = (velocity / 128) & 0x00FF;
|
||||
AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
|
||||
} else {
|
||||
/* Note off */
|
||||
AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
|
||||
}
|
||||
break;
|
||||
case MIDIST_CONTROLLER:
|
||||
b2 = this->songdata[chandata.playpos++];
|
||||
if (b1 == MIDICT_MODE_MONO) {
|
||||
/* Unknown what the purpose of this is.
|
||||
* Occurs in "Can't get There from Here" and in "Aliens Ate my Railway" a few times each.
|
||||
* Possibly intended to give hints to other (non-GM) music drivers decoding the song.
|
||||
*/
|
||||
break;
|
||||
} else if (b1 == 0) {
|
||||
/* Standard MIDI controller 0 is "bank select", override meaning to change tempo.
|
||||
* This is not actually used in any of the original songs. */
|
||||
if (b2 != 0) {
|
||||
this->current_tempo = ((int)b2) * 48 / 60;
|
||||
}
|
||||
break;
|
||||
} else if (b1 == MIDICT_EFFECTS1) {
|
||||
/* Override value of this controller, default mapping is Reverb Send Level according to MMA RP-023.
|
||||
* Unknown what the purpose of this particular value is. */
|
||||
b2 = 30;
|
||||
}
|
||||
AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
|
||||
break;
|
||||
case MIDIST_PROGCHG:
|
||||
if (b1 == 0x7E) {
|
||||
/* Program change to "Applause" is originally used
|
||||
* to cause the song to loop, but that gets handled
|
||||
* separately in the output driver here.
|
||||
* Just end the song. */
|
||||
this->shouldplayflag = false;
|
||||
break;
|
||||
}
|
||||
/* Used for note velocity scaling lookup */
|
||||
chandata.cur_program = b1;
|
||||
/* Two programs translated to a third, this is likely to
|
||||
* provide three different velocity scalings of "brass". */
|
||||
if (b1 == 0x57 || b1 == 0x3F) {
|
||||
b1 = 0x3E;
|
||||
}
|
||||
AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
|
||||
break;
|
||||
case MIDIST_PITCHBEND:
|
||||
b2 = this->songdata[chandata.playpos++];
|
||||
AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
newdelay = this->ReadVariableLength(chandata.playpos);
|
||||
} while (newdelay == 0);
|
||||
|
||||
return newdelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play one frame of data into a block.
|
||||
*/
|
||||
bool PlayFrame(MidiFile::DataBlock &block)
|
||||
{
|
||||
/* Update tempo/ticks counter */
|
||||
this->tempo_ticks -= this->current_tempo;
|
||||
if (this->tempo_ticks > 0) {
|
||||
return true;
|
||||
}
|
||||
this->tempo_ticks += TEMPO_RATE;
|
||||
|
||||
/* Look over all channels, play those active */
|
||||
for (int ch = 0; ch < 16; ch++) {
|
||||
Channel &chandata = this->channels[ch];
|
||||
if (chandata.playpos != 0) {
|
||||
if (chandata.delay == 0) {
|
||||
chandata.delay = this->PlayChannelFrame(block, ch);
|
||||
}
|
||||
chandata.delay--;
|
||||
}
|
||||
}
|
||||
|
||||
return this->shouldplayflag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform playback of whole song.
|
||||
*/
|
||||
bool PlayInto()
|
||||
{
|
||||
/* Tempo seems to be handled as TEMPO_RATE = 148 ticks per second.
|
||||
* Use this as the tickdiv, and define the tempo to be one second (1M microseconds) per tickdiv.
|
||||
* MIDI software loading exported files will show a bogus tempo, but playback will be correct. */
|
||||
this->target.tickdiv = TEMPO_RATE;
|
||||
this->target.tempos.push_back(MidiFile::TempoChange(0, 1000000));
|
||||
|
||||
/* Initialize playback simulation */
|
||||
this->RestartSong();
|
||||
this->shouldplayflag = true;
|
||||
this->current_tempo = (int32)this->initial_tempo * 24 / 60;
|
||||
this->tempo_ticks = this->current_tempo;
|
||||
|
||||
/* Always reset percussion channel to program 0 */
|
||||
this->target.blocks.push_back(MidiFile::DataBlock());
|
||||
AddMidiData(this->target.blocks.back(), MIDIST_PROGCHG+9, 0x00);
|
||||
|
||||
/* Technically should be an endless loop, but having
|
||||
* a maximum (about 10 minutes) avoids getting stuck,
|
||||
* in case of corrupted data. */
|
||||
for (uint32 tick = 0; tick < 100000; tick+=1) {
|
||||
this->target.blocks.push_back(MidiFile::DataBlock());
|
||||
auto &block = this->target.blocks.back();
|
||||
block.ticktime = tick;
|
||||
if (!this->PlayFrame(block)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
/** Frames/ticks per second for music playback */
|
||||
const int MpsMachine::TEMPO_RATE = 148;
|
||||
/** Base note velocities for various GM programs */
|
||||
const byte MpsMachine::programvelocities[128] = {
|
||||
100, 100, 100, 100, 100, 90, 100, 100, 100, 100, 100, 90, 100, 100, 100, 100,
|
||||
100, 100, 85, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 90, 110, 80,
|
||||
100, 100, 100, 90, 70, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 90, 100, 100, 100, 100, 100, 100, 120, 100, 100, 100, 120, 100, 127,
|
||||
100, 100, 90, 100, 100, 100, 100, 100, 100, 95, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 115, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
};
|
||||
|
||||
/**
|
||||
* Create MIDI data from song data for the original Microprose music drivers.
|
||||
* @param data pointer to block of data
|
||||
* @param length size of data in bytes
|
||||
* @return true if the data could be loaded
|
||||
*/
|
||||
bool MidiFile::LoadMpsData(const byte *data, size_t length)
|
||||
{
|
||||
_midifile_instance = this;
|
||||
|
||||
MpsMachine machine(data, length, *this);
|
||||
return machine.PlayInto() && FixupMidiData(*this);
|
||||
}
|
||||
|
||||
bool MidiFile::LoadSong(const MusicSongInfo &song)
|
||||
{
|
||||
switch (song.filetype) {
|
||||
case MTT_STANDARDMIDI:
|
||||
return this->LoadFile(song.filename);
|
||||
case MTT_MPSMIDI:
|
||||
{
|
||||
size_t songdatalen = 0;
|
||||
byte *songdata = GetMusicCatEntryData(song.filename, song.cat_index, songdatalen);
|
||||
if (songdata != NULL) {
|
||||
bool result = this->LoadMpsData(songdata, songdatalen);
|
||||
free(songdata);
|
||||
return result;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default:
|
||||
NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move data from other to this, and clears other.
|
||||
* @param other object containing loaded data to take over
|
||||
@@ -450,8 +839,280 @@ void MidiFile::MoveFrom(MidiFile &other)
|
||||
std::swap(this->tempos, other.tempos);
|
||||
this->tickdiv = other.tickdiv;
|
||||
|
||||
_midifile_instance = this;
|
||||
|
||||
other.blocks.clear();
|
||||
other.tempos.clear();
|
||||
other.tickdiv = 0;
|
||||
}
|
||||
|
||||
static void WriteVariableLen(FILE *f, uint32 value)
|
||||
{
|
||||
if (value < 0x7F) {
|
||||
byte tb = value;
|
||||
fwrite(&tb, 1, 1, f);
|
||||
} else if (value < 0x3FFF) {
|
||||
byte tb[2];
|
||||
tb[1] = value & 0x7F; value >>= 7;
|
||||
tb[0] = (value & 0x7F) | 0x80; value >>= 7;
|
||||
fwrite(tb, 1, sizeof(tb), f);
|
||||
} else if (value < 0x1FFFFF) {
|
||||
byte tb[3];
|
||||
tb[2] = value & 0x7F; value >>= 7;
|
||||
tb[1] = (value & 0x7F) | 0x80; value >>= 7;
|
||||
tb[0] = (value & 0x7F) | 0x80; value >>= 7;
|
||||
fwrite(tb, 1, sizeof(tb), f);
|
||||
} else if (value < 0x0FFFFFFF) {
|
||||
byte tb[4];
|
||||
tb[3] = value & 0x7F; value >>= 7;
|
||||
tb[2] = (value & 0x7F) | 0x80; value >>= 7;
|
||||
tb[1] = (value & 0x7F) | 0x80; value >>= 7;
|
||||
tb[0] = (value & 0x7F) | 0x80; value >>= 7;
|
||||
fwrite(tb, 1, sizeof(tb), f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a Standard MIDI File containing the decoded music.
|
||||
* @param filename Name of file to write to
|
||||
* @return True if the file was written to completion
|
||||
*/
|
||||
bool MidiFile::WriteSMF(const char *filename)
|
||||
{
|
||||
FILE *f = FioFOpenFile(filename, "wb", Subdirectory::NO_DIRECTORY);
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* SMF header */
|
||||
const byte fileheader[] = {
|
||||
'M', 'T', 'h', 'd', // block name
|
||||
0x00, 0x00, 0x00, 0x06, // BE32 block length, always 6 bytes
|
||||
0x00, 0x00, // writing format 0 (all in one track)
|
||||
0x00, 0x01, // containing 1 track (BE16)
|
||||
(byte)(this->tickdiv >> 8), (byte)this->tickdiv, // tickdiv in BE16
|
||||
};
|
||||
fwrite(fileheader, sizeof(fileheader), 1, f);
|
||||
|
||||
/* Track header */
|
||||
const byte trackheader[] = {
|
||||
'M', 'T', 'r', 'k', // block name
|
||||
0, 0, 0, 0, // BE32 block length, unknown at this time
|
||||
};
|
||||
fwrite(trackheader, sizeof(trackheader), 1, f);
|
||||
/* Determine position to write the actual track block length at */
|
||||
size_t tracksizepos = ftell(f) - 4;
|
||||
|
||||
/* Write blocks in sequence */
|
||||
uint32 lasttime = 0;
|
||||
size_t nexttempoindex = 0;
|
||||
for (size_t bi = 0; bi < this->blocks.size(); bi++) {
|
||||
DataBlock &block = this->blocks[bi];
|
||||
TempoChange &nexttempo = this->tempos[nexttempoindex];
|
||||
|
||||
uint32 timediff = block.ticktime - lasttime;
|
||||
|
||||
/* Check if there is a tempo change before this block */
|
||||
if (nexttempo.ticktime < block.ticktime) {
|
||||
timediff = nexttempo.ticktime - lasttime;
|
||||
}
|
||||
|
||||
/* Write delta time for block */
|
||||
lasttime += timediff;
|
||||
bool needtime = false;
|
||||
WriteVariableLen(f, timediff);
|
||||
|
||||
/* Write tempo change if there is one */
|
||||
if (nexttempo.ticktime <= block.ticktime) {
|
||||
byte tempobuf[6] = { MIDIST_SMF_META, 0x51, 0x03, 0, 0, 0 };
|
||||
tempobuf[3] = (nexttempo.tempo & 0x00FF0000) >> 16;
|
||||
tempobuf[4] = (nexttempo.tempo & 0x0000FF00) >> 8;
|
||||
tempobuf[5] = (nexttempo.tempo & 0x000000FF);
|
||||
fwrite(tempobuf, sizeof(tempobuf), 1, f);
|
||||
nexttempoindex++;
|
||||
needtime = true;
|
||||
}
|
||||
/* If a tempo change occurred between two blocks, rather than
|
||||
* at start of this one, start over with delta time for the block. */
|
||||
if (nexttempo.ticktime < block.ticktime) {
|
||||
/* Start loop over at same index */
|
||||
bi--;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Write each block data command */
|
||||
byte *dp = block.data.Begin();
|
||||
while (dp < block.data.End()) {
|
||||
/* Always zero delta time inside blocks */
|
||||
if (needtime) {
|
||||
fputc(0, f);
|
||||
}
|
||||
needtime = true;
|
||||
|
||||
/* Check message type and write appropriate number of bytes */
|
||||
switch (*dp & 0xF0) {
|
||||
case MIDIST_NOTEOFF:
|
||||
case MIDIST_NOTEON:
|
||||
case MIDIST_POLYPRESS:
|
||||
case MIDIST_CONTROLLER:
|
||||
case MIDIST_PITCHBEND:
|
||||
fwrite(dp, 1, 3, f);
|
||||
dp += 3;
|
||||
continue;
|
||||
case MIDIST_PROGCHG:
|
||||
case MIDIST_CHANPRESS:
|
||||
fwrite(dp, 1, 2, f);
|
||||
dp += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Sysex needs to measure length and write that as well */
|
||||
if (*dp == MIDIST_SYSEX) {
|
||||
fwrite(dp, 1, 1, f);
|
||||
dp++;
|
||||
byte *sysexend = dp;
|
||||
while (*sysexend != MIDIST_ENDSYSEX) sysexend++;
|
||||
ptrdiff_t sysexlen = sysexend - dp;
|
||||
WriteVariableLen(f, sysexlen);
|
||||
fwrite(dp, 1, sysexend - dp, f);
|
||||
dp = sysexend;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Fail for any other commands */
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* End of track marker */
|
||||
static const byte track_end_marker[] = { 0x00, MIDIST_SMF_META, 0x2F, 0x00 };
|
||||
fwrite(&track_end_marker, sizeof(track_end_marker), 1, f);
|
||||
|
||||
/* Fill out the RIFF block length */
|
||||
size_t trackendpos = ftell(f);
|
||||
fseek(f, tracksizepos, SEEK_SET);
|
||||
uint32 tracksize = (uint32)(trackendpos - tracksizepos - 4); // blindly assume we never produce files larger than 2 GB
|
||||
tracksize = TO_BE32(tracksize);
|
||||
fwrite(&tracksize, 4, 1, f);
|
||||
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of a Standard MIDI File for a given song.
|
||||
* For songs already in SMF format, just returns the original.
|
||||
* Otherwise the song is converted, written to a temporary-ish file, and the written filename is returned.
|
||||
* @param song Song definition to query
|
||||
* @return Full filename string, empty string if failed
|
||||
*/
|
||||
std::string MidiFile::GetSMFFile(const MusicSongInfo &song)
|
||||
{
|
||||
if (song.filetype == MTT_STANDARDMIDI) {
|
||||
return std::string(song.filename);
|
||||
}
|
||||
|
||||
if (song.filetype != MTT_MPSMIDI) return std::string();
|
||||
|
||||
const char *lastpathsep = strrchr(song.filename, PATHSEPCHAR);
|
||||
if (lastpathsep == NULL) {
|
||||
lastpathsep = song.filename;
|
||||
}
|
||||
|
||||
char basename[MAX_PATH];
|
||||
{
|
||||
/* Remove all '.' characters from filename */
|
||||
char *wp = basename;
|
||||
for (const char *rp = lastpathsep + 1; *rp != '\0'; rp++) {
|
||||
if (*rp != '.') *wp++ = *rp;
|
||||
}
|
||||
*wp++ = '\0';
|
||||
}
|
||||
|
||||
char tempdirname[MAX_PATH];
|
||||
FioGetFullPath(tempdirname, lastof(tempdirname), Searchpath::SP_AUTODOWNLOAD_DIR, Subdirectory::BASESET_DIR, basename);
|
||||
if (!AppendPathSeparator(tempdirname, lastof(tempdirname))) return std::string();
|
||||
FioCreateDirectory(tempdirname);
|
||||
|
||||
char output_filename[MAX_PATH];
|
||||
seprintf(output_filename, lastof(output_filename), "%s%d.mid", tempdirname, song.cat_index);
|
||||
|
||||
if (FileExists(output_filename)) {
|
||||
/* If the file already exists, assume it's the correct decoded data */
|
||||
return std::string(output_filename);
|
||||
}
|
||||
|
||||
byte *data;
|
||||
size_t datalen;
|
||||
data = GetMusicCatEntryData(song.filename, song.cat_index, datalen);
|
||||
if (data == NULL) return std::string();
|
||||
|
||||
MidiFile midifile;
|
||||
if (!midifile.LoadMpsData(data, datalen)) {
|
||||
free(data);
|
||||
return std::string();
|
||||
}
|
||||
free(data);
|
||||
|
||||
if (midifile.WriteSMF(output_filename)) {
|
||||
return std::string(output_filename);
|
||||
} else {
|
||||
return std::string();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool CmdDumpSMF(byte argc, char *argv[])
|
||||
{
|
||||
if (argc == 0) {
|
||||
IConsolePrint(CC_WARNING, "Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'");
|
||||
return true;
|
||||
}
|
||||
if (argc != 2) {
|
||||
IConsolePrint(CC_WARNING, "You must specify a filename to write MIDI data to.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_midifile_instance == NULL) {
|
||||
IConsolePrint(CC_ERROR, "There is no MIDI file loaded currently, make sure music is playing, and you're using a driver that works with raw MIDI.");
|
||||
return false;
|
||||
}
|
||||
|
||||
char fnbuf[MAX_PATH] = { 0 };
|
||||
if (seprintf(fnbuf, lastof(fnbuf), "%s%s", FiosGetScreenshotDir(), argv[1]) >= (int)lengthof(fnbuf)) {
|
||||
IConsolePrint(CC_ERROR, "Filename too long.");
|
||||
return false;
|
||||
}
|
||||
IConsolePrintF(CC_INFO, "Dumping MIDI to: %s", fnbuf);
|
||||
|
||||
if (_midifile_instance->WriteSMF(fnbuf)) {
|
||||
IConsolePrint(CC_INFO, "File written successfully.");
|
||||
return true;
|
||||
} else {
|
||||
IConsolePrint(CC_ERROR, "An error occurred writing MIDI file.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void RegisterConsoleMidiCommands()
|
||||
{
|
||||
static bool registered = false;
|
||||
if (!registered) {
|
||||
IConsoleCmdRegister("dumpsmf", CmdDumpSMF);
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
MidiFile::MidiFile()
|
||||
{
|
||||
RegisterConsoleMidiCommands();
|
||||
}
|
||||
|
||||
MidiFile::~MidiFile()
|
||||
{
|
||||
if (_midifile_instance == this) {
|
||||
_midifile_instance = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,9 @@
|
||||
#include "../core/smallvec_type.hpp"
|
||||
#include "midi.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
struct MusicSongInfo;
|
||||
|
||||
struct MidiFile {
|
||||
struct DataBlock {
|
||||
@@ -34,9 +37,17 @@ struct MidiFile {
|
||||
std::vector<TempoChange> tempos; ///< list of tempo changes in file
|
||||
uint16 tickdiv; ///< ticks per quarter note
|
||||
|
||||
MidiFile();
|
||||
~MidiFile();
|
||||
|
||||
bool LoadFile(const char *filename);
|
||||
bool LoadMpsData(const byte *data, size_t length);
|
||||
bool LoadSong(const MusicSongInfo &song);
|
||||
void MoveFrom(MidiFile &other);
|
||||
|
||||
bool WriteSMF(const char *filename);
|
||||
|
||||
static std::string GetSMFFile(const MusicSongInfo &song);
|
||||
static bool ReadSMFHeader(const char *filename, SMFHeader &header);
|
||||
static bool ReadSMFHeader(FILE *file, SMFHeader &header);
|
||||
};
|
||||
|
@@ -14,6 +14,8 @@
|
||||
|
||||
#include "../driver.h"
|
||||
|
||||
struct MusicSongInfo;
|
||||
|
||||
/** Driver for all music playback. */
|
||||
class MusicDriver : public Driver {
|
||||
public:
|
||||
@@ -21,7 +23,7 @@ public:
|
||||
* Play a particular song.
|
||||
* @param filename The name of file with the song to play.
|
||||
*/
|
||||
virtual void PlaySong(const char *filename) = 0;
|
||||
virtual void PlaySong(const MusicSongInfo &song) = 0;
|
||||
|
||||
/**
|
||||
* Stop playing the current song.
|
||||
|
@@ -21,7 +21,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop() { }
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename) { }
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song) { }
|
||||
|
||||
/* virtual */ void StopSong() { }
|
||||
|
||||
|
@@ -12,6 +12,8 @@
|
||||
#include "../stdafx.h"
|
||||
#include "../openttd.h"
|
||||
#include "os2_m.h"
|
||||
#include "midifile.hpp"
|
||||
#include "../base_media_base.h"
|
||||
|
||||
#define INCL_DOS
|
||||
#define INCL_OS2MM
|
||||
@@ -49,11 +51,14 @@ static long CDECL MidiSendCommand(const char *cmd, ...)
|
||||
/** OS/2's music player's factory. */
|
||||
static FMusicDriver_OS2 iFMusicDriver_OS2;
|
||||
|
||||
void MusicDriver_OS2::PlaySong(const char *filename)
|
||||
void MusicDriver_OS2::PlaySong(const MusicSongInfo &song)
|
||||
{
|
||||
MidiSendCommand("close all");
|
||||
std::string filename = MidiFile::GetSMFFile(song);
|
||||
|
||||
if (MidiSendCommand("open %s type sequencer alias song", filename) != 0) {
|
||||
MidiSendCommand("close all");
|
||||
if (filename.empty()) return;
|
||||
|
||||
if (MidiSendCommand("open %s type sequencer alias song", filename.c_str()) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop();
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename);
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song);
|
||||
|
||||
/* virtual */ void StopSong();
|
||||
|
||||
|
@@ -30,7 +30,9 @@
|
||||
|
||||
#include "../stdafx.h"
|
||||
#include "qtmidi.h"
|
||||
#include "midifile.hpp"
|
||||
#include "../debug.h"
|
||||
#include "../base_media_base.h"
|
||||
|
||||
#define Rect OTTDRect
|
||||
#define Point OTTDPoint
|
||||
@@ -258,11 +260,14 @@ void MusicDriver_QtMidi::Stop()
|
||||
*
|
||||
* @param filename Path to a MIDI file.
|
||||
*/
|
||||
void MusicDriver_QtMidi::PlaySong(const char *filename)
|
||||
void MusicDriver_QtMidi::PlaySong(const MusicSongInfo &song)
|
||||
{
|
||||
if (!_quicktime_started) return;
|
||||
|
||||
DEBUG(driver, 2, "qtmidi: trying to play '%s'", filename);
|
||||
std::string filename = MidiFile::GetSMFFile(song);
|
||||
if (filename.empty()) return;
|
||||
|
||||
DEBUG(driver, 2, "qtmidi: trying to play '%s'", filename.c_str());
|
||||
switch (_quicktime_state) {
|
||||
case QT_STATE_PLAY:
|
||||
StopSong();
|
||||
@@ -276,12 +281,12 @@ void MusicDriver_QtMidi::PlaySong(const char *filename)
|
||||
FALLTHROUGH;
|
||||
|
||||
case QT_STATE_IDLE:
|
||||
LoadMovieForMIDIFile(filename, &_quicktime_movie);
|
||||
LoadMovieForMIDIFile(filename.c_str(), &_quicktime_movie);
|
||||
SetMovieVolume(_quicktime_movie, VOLUME);
|
||||
StartMovie(_quicktime_movie);
|
||||
_quicktime_state = QT_STATE_PLAY;
|
||||
}
|
||||
DEBUG(driver, 3, "qtmidi: playing '%s'", filename);
|
||||
DEBUG(driver, 3, "qtmidi: playing '%s'", filename.c_str());
|
||||
}
|
||||
|
||||
|
||||
|
@@ -20,7 +20,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop();
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename);
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song);
|
||||
|
||||
/* virtual */ void StopSong();
|
||||
|
||||
|
@@ -18,6 +18,7 @@
|
||||
#include "../debug.h"
|
||||
#include "midifile.hpp"
|
||||
#include "midi.h"
|
||||
#include "../base_media_base.h"
|
||||
|
||||
#include "../safeguards.h"
|
||||
|
||||
@@ -304,12 +305,16 @@ void CALLBACK TimerCallback(UINT uTimerID, UINT, DWORD_PTR dwUser, DWORD_PTR, DW
|
||||
}
|
||||
}
|
||||
|
||||
void MusicDriver_Win32::PlaySong(const char *filename)
|
||||
void MusicDriver_Win32::PlaySong(const MusicSongInfo &song)
|
||||
{
|
||||
DEBUG(driver, 2, "Win32-MIDI: PlaySong: entry");
|
||||
EnterCriticalSection(&_midi.lock);
|
||||
|
||||
_midi.next_file.LoadFile(filename);
|
||||
if (!_midi.next_file.LoadSong(song)) {
|
||||
LeaveCriticalSection(&_midi.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
_midi.next_segment.start = 0;
|
||||
_midi.next_segment.end = 0;
|
||||
_midi.next_segment.loop = false;
|
||||
|
@@ -21,7 +21,7 @@ public:
|
||||
|
||||
/* virtual */ void Stop();
|
||||
|
||||
/* virtual */ void PlaySong(const char *filename);
|
||||
/* virtual */ void PlaySong(const MusicSongInfo &song);
|
||||
|
||||
/* virtual */ void StopSong();
|
||||
|
||||
|
@@ -42,7 +42,7 @@
|
||||
*/
|
||||
static const char *GetSongName(int index)
|
||||
{
|
||||
return BaseMusic::GetUsedSet()->song_name[index];
|
||||
return BaseMusic::GetUsedSet()->songinfo[index].songname;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +52,7 @@ static const char *GetSongName(int index)
|
||||
*/
|
||||
static int GetTrackNumber(int index)
|
||||
{
|
||||
return BaseMusic::GetUsedSet()->track_nr[index];
|
||||
return BaseMusic::GetUsedSet()->songinfo[index].tracknr;
|
||||
}
|
||||
|
||||
/** The currently played song */
|
||||
@@ -186,10 +186,12 @@ static void MusicVolumeChanged(byte new_vol)
|
||||
static void DoPlaySong()
|
||||
{
|
||||
char filename[MAX_PATH];
|
||||
if (FioFindFullPath(filename, lastof(filename), BASESET_DIR, BaseMusic::GetUsedSet()->files[_music_wnd_cursong - 1].filename) == NULL) {
|
||||
FioFindFullPath(filename, lastof(filename), OLD_GM_DIR, BaseMusic::GetUsedSet()->files[_music_wnd_cursong - 1].filename);
|
||||
MusicSongInfo songinfo = BaseMusic::GetUsedSet()->songinfo[_music_wnd_cursong - 1]; // copy
|
||||
if (FioFindFullPath(filename, lastof(filename), BASESET_DIR, songinfo.filename) == NULL) {
|
||||
FioFindFullPath(filename, lastof(filename), OLD_GM_DIR, songinfo.filename);
|
||||
}
|
||||
MusicDriver::GetInstance()->PlaySong(filename);
|
||||
songinfo.filename = filename; // non-owned pointer
|
||||
MusicDriver::GetInstance()->PlaySong(songinfo);
|
||||
SetWindowDirty(WC_MUSIC_WINDOW, 0);
|
||||
}
|
||||
|
||||
|
610
src/os/windows/string_uniscribe.cpp
Normal file
610
src/os/windows/string_uniscribe.cpp
Normal file
@@ -0,0 +1,610 @@
|
||||
/* $Id$ */
|
||||
|
||||
/*
|
||||
* This file is part of OpenTTD.
|
||||
* OpenTTD 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, version 2.
|
||||
* OpenTTD 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 OpenTTD. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @file string_uniscribe.cpp Functions related to laying out text on Win32. */
|
||||
|
||||
#if defined(WITH_UNISCRIBE)
|
||||
|
||||
#include "../../stdafx.h"
|
||||
#include "../../debug.h"
|
||||
#include "string_uniscribe.h"
|
||||
#include "../../language.h"
|
||||
#include "../../strings_func.h"
|
||||
#include "../../string_func.h"
|
||||
#include "../../table/control_codes.h"
|
||||
#include "win32.h"
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <usp10.h>
|
||||
|
||||
#include "../../safeguards.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
# pragma comment(lib, "usp10")
|
||||
#endif
|
||||
|
||||
|
||||
/** Uniscribe cache for internal font information, cleared when OTTD changes fonts. */
|
||||
static SCRIPT_CACHE _script_cache[FS_END];
|
||||
|
||||
/**
|
||||
* Contains all information about a run of characters. A run are consecutive
|
||||
* characters that share a single font and language.
|
||||
*/
|
||||
struct UniscribeRun {
|
||||
int pos;
|
||||
int len;
|
||||
Font *font;
|
||||
|
||||
std::vector<GlyphID> ft_glyphs;
|
||||
|
||||
SCRIPT_ANALYSIS sa;
|
||||
std::vector<WORD> char_to_glyph;
|
||||
|
||||
std::vector<SCRIPT_VISATTR> vis_attribs;
|
||||
std::vector<WORD> glyphs;
|
||||
std::vector<int> advances;
|
||||
std::vector<GOFFSET> offsets;
|
||||
int total_advance;
|
||||
|
||||
UniscribeRun(int pos, int len, Font *font, SCRIPT_ANALYSIS &sa) : pos(pos), len(len), font(font), sa(sa) {}
|
||||
};
|
||||
|
||||
/** Break a string into language formatting ranges. */
|
||||
static std::vector<SCRIPT_ITEM> UniscribeItemizeString(UniscribeParagraphLayoutFactory::CharType *buff, int32 length);
|
||||
/** Generate and place glyphs for a run of characters. */
|
||||
static bool UniscribeShapeRun(const UniscribeParagraphLayoutFactory::CharType *buff, UniscribeRun &range);
|
||||
|
||||
/**
|
||||
* Wrapper for doing layouts with Uniscribe.
|
||||
*/
|
||||
class UniscribeParagraphLayout : public ParagraphLayouter {
|
||||
private:
|
||||
const UniscribeParagraphLayoutFactory::CharType *text_buffer;
|
||||
|
||||
std::vector<UniscribeRun> ranges; ///< All runs of the text.
|
||||
std::vector<UniscribeRun>::iterator cur_range; ///< The next run to be output.
|
||||
int cur_range_offset = 0; ///< Offset from the start of the current run from where to output.
|
||||
|
||||
public:
|
||||
/** Visual run contains data about the bit of text with the same font. */
|
||||
class UniscribeVisualRun : public ParagraphLayouter::VisualRun {
|
||||
private:
|
||||
std::vector<GlyphID> glyphs;
|
||||
std::vector<float> positions;
|
||||
std::vector<WORD> char_to_glyph;
|
||||
|
||||
int start_pos;
|
||||
int total_advance;
|
||||
int num_glyphs;
|
||||
Font *font;
|
||||
|
||||
mutable int *glyph_to_char = NULL;
|
||||
|
||||
public:
|
||||
UniscribeVisualRun(const UniscribeRun &range, int x);
|
||||
virtual ~UniscribeVisualRun()
|
||||
{
|
||||
free(this->glyph_to_char);
|
||||
}
|
||||
|
||||
virtual const GlyphID *GetGlyphs() const { return &this->glyphs[0]; }
|
||||
virtual const float *GetPositions() const { return &this->positions[0]; }
|
||||
virtual const int *GetGlyphToCharMap() const;
|
||||
|
||||
virtual const Font *GetFont() const { return this->font; }
|
||||
virtual int GetLeading() const { return this->font->fc->GetHeight(); }
|
||||
virtual int GetGlyphCount() const { return this->num_glyphs; }
|
||||
int GetAdvance() const { return this->total_advance; }
|
||||
};
|
||||
|
||||
/** A single line worth of VisualRuns. */
|
||||
class UniscribeLine : public AutoDeleteSmallVector<UniscribeVisualRun *, 4>, public ParagraphLayouter::Line {
|
||||
public:
|
||||
virtual int GetLeading() const;
|
||||
virtual int GetWidth() const;
|
||||
virtual int CountRuns() const { return this->Length(); }
|
||||
virtual const VisualRun *GetVisualRun(int run) const { return *this->Get(run); }
|
||||
|
||||
int GetInternalCharLength(WChar c) const
|
||||
{
|
||||
/* Uniscribe uses UTF-16 internally which means we need to account for surrogate pairs. */
|
||||
return c >= 0x010000U ? 2 : 1;
|
||||
}
|
||||
};
|
||||
|
||||
UniscribeParagraphLayout(std::vector<UniscribeRun> &ranges, const UniscribeParagraphLayoutFactory::CharType *buffer) : text_buffer(buffer), ranges(ranges)
|
||||
{
|
||||
this->Reflow();
|
||||
}
|
||||
|
||||
virtual ~UniscribeParagraphLayout() {}
|
||||
|
||||
virtual void Reflow()
|
||||
{
|
||||
this->cur_range = this->ranges.begin();
|
||||
this->cur_range_offset = 0;
|
||||
}
|
||||
|
||||
virtual const Line *NextLine(int max_width);
|
||||
};
|
||||
|
||||
void UniscribeResetScriptCache(FontSize size)
|
||||
{
|
||||
if (_script_cache[size] != NULL) {
|
||||
ScriptFreeCache(&_script_cache[size]);
|
||||
_script_cache[size] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/** Load the matching native Windows font. */
|
||||
static HFONT HFontFromFont(Font *font)
|
||||
{
|
||||
LOGFONT logfont;
|
||||
ZeroMemory(&logfont, sizeof(LOGFONT));
|
||||
logfont.lfHeight = font->fc->GetHeight();
|
||||
logfont.lfWeight = FW_NORMAL;
|
||||
logfont.lfCharSet = DEFAULT_CHARSET;
|
||||
convert_to_fs(font->fc->GetFontName(), logfont.lfFaceName, lengthof(logfont.lfFaceName));
|
||||
|
||||
return CreateFontIndirect(&logfont);
|
||||
}
|
||||
|
||||
/** Determine the glyph positions for a run. */
|
||||
static bool UniscribeShapeRun(const UniscribeParagraphLayoutFactory::CharType *buff, UniscribeRun &range)
|
||||
{
|
||||
/* Initial size guess for the number of glyphs recommended by Uniscribe. */
|
||||
range.glyphs.resize(range.len * 3 / 2 + 16);
|
||||
range.vis_attribs.resize(range.glyphs.size());
|
||||
|
||||
/* The char-to-glyph array is the same size as the input. */
|
||||
range.char_to_glyph.resize(range.len);
|
||||
|
||||
HDC temp_dc = NULL;
|
||||
HFONT old_font = NULL;
|
||||
HFONT cur_font = NULL;
|
||||
|
||||
while (true) {
|
||||
/* Shape the text run by determining the glyphs needed for display. */
|
||||
int glyphs_used = 0;
|
||||
HRESULT hr = ScriptShape(temp_dc, &_script_cache[range.font->fc->GetSize()], buff + range.pos, range.len, (int)range.glyphs.size(), &range.sa, &range.glyphs[0], &range.char_to_glyph[0], &range.vis_attribs[0], &glyphs_used);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
range.glyphs.resize(glyphs_used);
|
||||
range.vis_attribs.resize(glyphs_used);
|
||||
|
||||
/* Calculate the glyph positions. */
|
||||
ABC abc;
|
||||
range.advances.resize(range.glyphs.size());
|
||||
range.offsets.resize(range.glyphs.size());
|
||||
hr = ScriptPlace(temp_dc, &_script_cache[range.font->fc->GetSize()], &range.glyphs[0], (int)range.glyphs.size(), &range.vis_attribs[0], &range.sa, &range.advances[0], &range.offsets[0], &abc);
|
||||
if (SUCCEEDED(hr)) {
|
||||
/* We map our special sprite chars to values that don't fit into a WORD. Copy the glyphs
|
||||
* into a new vector and query the real glyph to use for these special chars. */
|
||||
range.ft_glyphs.resize(range.glyphs.size());
|
||||
for (size_t g_id = 0; g_id < range.glyphs.size(); g_id++) {
|
||||
range.ft_glyphs[g_id] = range.glyphs[g_id];
|
||||
}
|
||||
for (int i = 0; i < range.len; i++) {
|
||||
if (buff[range.pos + i] >= SCC_SPRITE_START && buff[range.pos + i] <= SCC_SPRITE_END) {
|
||||
range.ft_glyphs[range.char_to_glyph[i]] = range.font->fc->MapCharToGlyph(buff[range.pos + i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* FreeType and GDI/Uniscribe seems to occasionally disagree over the width of a glyph. */
|
||||
range.total_advance = 0;
|
||||
for (size_t i = 0; i < range.advances.size(); i++) {
|
||||
if (range.advances[i] > 0 && range.ft_glyphs[i] != 0xFFFF) range.advances[i] = range.font->fc->GetGlyphWidth(range.ft_glyphs[i]);
|
||||
range.total_advance += range.advances[i];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hr == E_OUTOFMEMORY) {
|
||||
/* The glyph buffer needs to be larger. Just double it every time. */
|
||||
range.glyphs.resize(range.glyphs.size() * 2);
|
||||
range.vis_attribs.resize(range.vis_attribs.size() * 2);
|
||||
} else if (hr == E_PENDING) {
|
||||
/* Glyph data is not in cache, load native font. */
|
||||
cur_font = HFontFromFont(range.font);
|
||||
if (cur_font == NULL) return false; // Sorry, no dice.
|
||||
|
||||
temp_dc = CreateCompatibleDC(NULL);
|
||||
SetMapMode(temp_dc, MM_TEXT);
|
||||
old_font = (HFONT)SelectObject(temp_dc, cur_font);
|
||||
} else if (hr == USP_E_SCRIPT_NOT_IN_FONT && range.sa.eScript != SCRIPT_UNDEFINED) {
|
||||
/* Try again with the generic shaping engine. */
|
||||
range.sa.eScript = SCRIPT_UNDEFINED;
|
||||
} else {
|
||||
/* Some unknown other error. */
|
||||
if (temp_dc != NULL) {
|
||||
SelectObject(temp_dc, old_font);
|
||||
DeleteObject(cur_font);
|
||||
ReleaseDC(NULL, temp_dc);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (temp_dc != NULL) {
|
||||
SelectObject(temp_dc, old_font);
|
||||
DeleteObject(cur_font);
|
||||
ReleaseDC(NULL, temp_dc);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::vector<SCRIPT_ITEM> UniscribeItemizeString(UniscribeParagraphLayoutFactory::CharType *buff, int32 length)
|
||||
{
|
||||
/* Itemize text. */
|
||||
SCRIPT_CONTROL control;
|
||||
ZeroMemory(&control, sizeof(SCRIPT_CONTROL));
|
||||
control.uDefaultLanguage = _current_language->winlangid;
|
||||
|
||||
SCRIPT_STATE state;
|
||||
ZeroMemory(&state, sizeof(SCRIPT_STATE));
|
||||
state.uBidiLevel = _current_text_dir == TD_RTL ? 1 : 0;
|
||||
|
||||
std::vector<SCRIPT_ITEM> items(16);
|
||||
while (true) {
|
||||
/* We subtract one from max_items to work around a buffer overflow on some older versions of Windows. */
|
||||
int generated = 0;
|
||||
HRESULT hr = ScriptItemize(buff, length, (int)items.size() - 1, &control, &state, &items[0], &generated);
|
||||
|
||||
if (SUCCEEDED(hr)) {
|
||||
/* Resize the item buffer. Note that Uniscribe will always add an additional end sentinel item. */
|
||||
items.resize(generated + 1);
|
||||
break;
|
||||
}
|
||||
/* Some kind of error except item buffer too small. */
|
||||
if (hr != E_OUTOFMEMORY) return std::vector<SCRIPT_ITEM>();
|
||||
|
||||
items.resize(items.size() * 2);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/* static */ ParagraphLayouter *UniscribeParagraphLayoutFactory::GetParagraphLayout(CharType *buff, CharType *buff_end, FontMap &fontMapping)
|
||||
{
|
||||
int32 length = buff_end - buff;
|
||||
/* Can't layout an empty string. */
|
||||
if (length == 0) return NULL;
|
||||
|
||||
/* Can't layout our in-built sprite fonts. */
|
||||
for (FontMap::const_iterator i = fontMapping.Begin(); i != fontMapping.End(); i++) {
|
||||
if (i->second->fc->IsBuiltInFont()) return NULL;
|
||||
}
|
||||
|
||||
/* Itemize text. */
|
||||
std::vector<SCRIPT_ITEM> items = UniscribeItemizeString(buff, length);
|
||||
if (items.size() == 0) return NULL;
|
||||
|
||||
/* Build ranges from the items and the font map. A range is a run of text
|
||||
* that is part of a single item and formatted using a single font style. */
|
||||
std::vector<UniscribeRun> ranges;
|
||||
|
||||
int cur_pos = 0;
|
||||
std::vector<SCRIPT_ITEM>::iterator cur_item = items.begin();
|
||||
for (FontMap::const_iterator i = fontMapping.Begin(); i != fontMapping.End(); i++) {
|
||||
while (cur_pos < i->first && cur_item != items.end() - 1) {
|
||||
/* Add a range that spans the intersection of the remaining item and font run. */
|
||||
int stop_pos = min(i->first, (cur_item + 1)->iCharPos);
|
||||
assert(stop_pos - cur_pos > 0);
|
||||
ranges.push_back(UniscribeRun(cur_pos, stop_pos - cur_pos, i->second, cur_item->a));
|
||||
|
||||
/* Shape the range. */
|
||||
if (!UniscribeShapeRun(buff, ranges.back())) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* If we are at the end of the current item, advance to the next item. */
|
||||
if (stop_pos == (cur_item + 1)->iCharPos) cur_item++;
|
||||
cur_pos = stop_pos;
|
||||
}
|
||||
}
|
||||
|
||||
return new UniscribeParagraphLayout(ranges, buff);
|
||||
}
|
||||
|
||||
/* virtual */ const ParagraphLayouter::Line *UniscribeParagraphLayout::NextLine(int max_width)
|
||||
{
|
||||
std::vector<UniscribeRun>::iterator start_run = this->cur_range;
|
||||
std::vector<UniscribeRun>::iterator last_run = this->cur_range;
|
||||
|
||||
if (start_run == this->ranges.end()) return NULL;
|
||||
|
||||
/* Add remaining width of the first run if it is a broken run. */
|
||||
int cur_width = 0;
|
||||
if (this->cur_range_offset != 0) {
|
||||
std::vector<int> dx(start_run->len);
|
||||
ScriptGetLogicalWidths(&start_run->sa, start_run->len, (int)start_run->glyphs.size(), &start_run->advances[0], &start_run->char_to_glyph[0], &start_run->vis_attribs[0], &dx[0]);
|
||||
|
||||
for (std::vector<int>::const_iterator c = dx.begin() + this->cur_range_offset; c != dx.end(); c++) {
|
||||
cur_width += *c;
|
||||
}
|
||||
++last_run;
|
||||
}
|
||||
|
||||
/* Gather runs until the line is full. */
|
||||
while (last_run != this->ranges.end() && cur_width < max_width) {
|
||||
cur_width += last_run->total_advance;
|
||||
++last_run;
|
||||
}
|
||||
|
||||
/* If the text does not fit into the available width, find a suitable breaking point. */
|
||||
int remaing_offset = (last_run - 1)->len;
|
||||
if (cur_width > max_width) {
|
||||
std::vector<SCRIPT_LOGATTR> log_attribs;
|
||||
|
||||
/* Get word break information. */
|
||||
int width_avail = max_width;
|
||||
int num_chars = this->cur_range_offset;
|
||||
int start_offs = this->cur_range_offset;
|
||||
int last_cluster = this->cur_range_offset + 1;
|
||||
for (std::vector<UniscribeRun>::iterator r = start_run; r != last_run; r++) {
|
||||
log_attribs.resize(r->pos - start_run->pos + r->len);
|
||||
if (FAILED(ScriptBreak(this->text_buffer + r->pos + start_offs, r->len - start_offs, &r->sa, &log_attribs[r->pos - start_run->pos + start_offs]))) return NULL;
|
||||
|
||||
std::vector<int> dx(r->len);
|
||||
ScriptGetLogicalWidths(&r->sa, r->len, (int)r->glyphs.size(), &r->advances[0], &r->char_to_glyph[0], &r->vis_attribs[0], &dx[0]);
|
||||
|
||||
/* Count absolute max character count on the line. */
|
||||
for (int c = start_offs; c < r->len && width_avail > 0; c++, num_chars++) {
|
||||
if (c > start_offs && log_attribs[num_chars].fCharStop) last_cluster = num_chars;
|
||||
width_avail -= dx[c];
|
||||
}
|
||||
|
||||
start_offs = 0;
|
||||
}
|
||||
|
||||
/* Walk backwards to find the last suitable breaking point. */
|
||||
while (--num_chars > this->cur_range_offset && !log_attribs[num_chars].fSoftBreak && !log_attribs[num_chars].fWhiteSpace) {}
|
||||
|
||||
if (num_chars == this->cur_range_offset) {
|
||||
/* Didn't find any suitable word break point, just break on the last cluster boundary. */
|
||||
num_chars = last_cluster;
|
||||
}
|
||||
|
||||
/* Include whitespace characters after the breaking point. */
|
||||
while (num_chars < (int)log_attribs.size() && log_attribs[num_chars].fWhiteSpace) {
|
||||
num_chars++;
|
||||
}
|
||||
|
||||
/* Get last run that corresponds to the number of characters to show. */
|
||||
for (std::vector<UniscribeRun>::iterator run = start_run; run != last_run; run++) {
|
||||
num_chars -= run->len;
|
||||
|
||||
if (num_chars <= 0) {
|
||||
remaing_offset = num_chars + run->len + 1;
|
||||
last_run = run + 1;
|
||||
assert(remaing_offset - 1 > 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Build display order from the runs. */
|
||||
std::vector<BYTE> bidi_level;
|
||||
for (std::vector<UniscribeRun>::iterator r = start_run; r != last_run; r++) {
|
||||
bidi_level.push_back(r->sa.s.uBidiLevel);
|
||||
}
|
||||
std::vector<INT> vis_to_log(bidi_level.size());
|
||||
if (FAILED(ScriptLayout((int)bidi_level.size(), &bidi_level[0], &vis_to_log[0], NULL))) return NULL;
|
||||
|
||||
/* Create line. */
|
||||
UniscribeLine *line = new UniscribeLine();
|
||||
|
||||
int cur_pos = 0;
|
||||
for (std::vector<INT>::iterator l = vis_to_log.begin(); l != vis_to_log.end(); l++) {
|
||||
std::vector<UniscribeRun>::iterator i_run = start_run + *l;
|
||||
UniscribeRun run = *i_run;
|
||||
|
||||
/* Partial run after line break (either start or end)? Reshape run to get the first/last glyphs right. */
|
||||
if (i_run == last_run - 1 && remaing_offset < (last_run - 1)->len) {
|
||||
run.len = remaing_offset - 1;
|
||||
|
||||
if (!UniscribeShapeRun(this->text_buffer, run)) return NULL;
|
||||
}
|
||||
if (i_run == start_run && this->cur_range_offset > 0) {
|
||||
assert(run.len - this->cur_range_offset > 0);
|
||||
run.pos += this->cur_range_offset;
|
||||
run.len -= this->cur_range_offset;
|
||||
|
||||
if (!UniscribeShapeRun(this->text_buffer, run)) return NULL;
|
||||
}
|
||||
|
||||
*line->Append() = new UniscribeVisualRun(run, cur_pos);
|
||||
cur_pos += run.total_advance;
|
||||
}
|
||||
|
||||
if (remaing_offset < (last_run - 1)->len) {
|
||||
/* We didn't use up all of the last run, store remainder for the next line. */
|
||||
this->cur_range_offset = remaing_offset - 1;
|
||||
this->cur_range = last_run - 1;
|
||||
assert(this->cur_range->len > this->cur_range_offset);
|
||||
} else {
|
||||
this->cur_range_offset = 0;
|
||||
this->cur_range = last_run;
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the height of the line.
|
||||
* @return The maximum height of the line.
|
||||
*/
|
||||
int UniscribeParagraphLayout::UniscribeLine::GetLeading() const
|
||||
{
|
||||
int leading = 0;
|
||||
for (const UniscribeVisualRun * const *run = this->Begin(); run != this->End(); run++) {
|
||||
leading = max(leading, (*run)->GetLeading());
|
||||
}
|
||||
|
||||
return leading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the width of this line.
|
||||
* @return The width of the line.
|
||||
*/
|
||||
int UniscribeParagraphLayout::UniscribeLine::GetWidth() const
|
||||
{
|
||||
int length = 0;
|
||||
for (const UniscribeVisualRun * const *run = this->Begin(); run != this->End(); run++) {
|
||||
length += (*run)->GetAdvance();
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
UniscribeParagraphLayout::UniscribeVisualRun::UniscribeVisualRun(const UniscribeRun &range, int x) : glyphs(range.ft_glyphs), char_to_glyph(range.char_to_glyph), start_pos(range.pos), total_advance(range.total_advance), font(range.font)
|
||||
{
|
||||
this->num_glyphs = (int)glyphs.size();
|
||||
this->positions.resize(this->num_glyphs * 2 + 2);
|
||||
|
||||
int advance = 0;
|
||||
for (int i = 0; i < this->num_glyphs; i++) {
|
||||
this->positions[i * 2 + 0] = range.offsets[i].du + advance + x;
|
||||
this->positions[i * 2 + 1] = range.offsets[i].dv;
|
||||
|
||||
advance += range.advances[i];
|
||||
}
|
||||
this->positions[this->num_glyphs * 2] = advance + x;
|
||||
}
|
||||
|
||||
const int *UniscribeParagraphLayout::UniscribeVisualRun::GetGlyphToCharMap() const
|
||||
{
|
||||
if (this->glyph_to_char == NULL) {
|
||||
this->glyph_to_char = CallocT<int>(this->GetGlyphCount());
|
||||
|
||||
/* The char to glyph array contains the first glyph index of the cluster that is associated
|
||||
* with each character. It is possible for a cluster to be formed of several chars. */
|
||||
for (int c = 0; c < (int)this->char_to_glyph.size(); c++) {
|
||||
/* If multiple chars map to one glyph, only refer back to the first character. */
|
||||
if (this->glyph_to_char[this->char_to_glyph[c]] == 0) this->glyph_to_char[this->char_to_glyph[c]] = c + this->start_pos;
|
||||
}
|
||||
|
||||
/* We only marked the first glyph of each cluster in the loop above. Fill the gaps. */
|
||||
int last_char = this->glyph_to_char[0];
|
||||
for (int g = 0; g < this->GetGlyphCount(); g++) {
|
||||
if (this->glyph_to_char[g] != 0) last_char = this->glyph_to_char[g];
|
||||
this->glyph_to_char[g] = last_char;
|
||||
}
|
||||
}
|
||||
|
||||
return this->glyph_to_char;
|
||||
}
|
||||
|
||||
|
||||
/* virtual */ void UniscribeStringIterator::SetString(const char *s)
|
||||
{
|
||||
const char *string_base = s;
|
||||
|
||||
this->utf16_to_utf8.clear();
|
||||
this->str_info.clear();
|
||||
this->cur_pos = 0;
|
||||
|
||||
/* Uniscribe operates on UTF-16, thus we have to convert the input string.
|
||||
* To be able to return proper offsets, we have to create a mapping at the same time. */
|
||||
std::vector<wchar_t> utf16_str; ///< UTF-16 copy of the string.
|
||||
while (*s != '\0') {
|
||||
size_t idx = s - string_base;
|
||||
|
||||
WChar c = Utf8Consume(&s);
|
||||
if (c < 0x10000) {
|
||||
utf16_str.push_back((wchar_t)c);
|
||||
} else {
|
||||
/* Make a surrogate pair. */
|
||||
utf16_str.push_back((wchar_t)(0xD800 + ((c - 0x10000) >> 10)));
|
||||
utf16_str.push_back((wchar_t)(0xDC00 + ((c - 0x10000) & 0x3FF)));
|
||||
this->utf16_to_utf8.push_back(idx);
|
||||
}
|
||||
this->utf16_to_utf8.push_back(idx);
|
||||
}
|
||||
this->utf16_to_utf8.push_back(s - string_base);
|
||||
|
||||
/* Query Uniscribe for word and cluster break information. */
|
||||
this->str_info.resize(utf16_to_utf8.size());
|
||||
|
||||
if (utf16_str.size() > 0) {
|
||||
/* Itemize string into language runs. */
|
||||
std::vector<SCRIPT_ITEM> runs = UniscribeItemizeString(&utf16_str[0], (int32)utf16_str.size());
|
||||
|
||||
for (std::vector<SCRIPT_ITEM>::const_iterator run = runs.begin(); runs.size() > 0 && run != runs.end() - 1; run++) {
|
||||
/* Get information on valid word and character break.s */
|
||||
int len = (run + 1)->iCharPos - run->iCharPos;
|
||||
std::vector<SCRIPT_LOGATTR> attr(len);
|
||||
ScriptBreak(&utf16_str[run->iCharPos], len, &run->a, &attr[0]);
|
||||
|
||||
/* Extract the information we're interested in. */
|
||||
for (size_t c = 0; c < attr.size(); c++) {
|
||||
/* First character of a run is always a valid word break. */
|
||||
this->str_info[c + run->iCharPos].word_stop = attr[c].fWordStop || c == 0;
|
||||
this->str_info[c + run->iCharPos].char_stop = attr[c].fCharStop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* End-of-string is always a valid stopping point. */
|
||||
this->str_info.back().char_stop = true;
|
||||
this->str_info.back().word_stop = true;
|
||||
}
|
||||
|
||||
/* virtual */ size_t UniscribeStringIterator::SetCurPosition(size_t pos)
|
||||
{
|
||||
/* Convert incoming position to an UTF-16 string index. */
|
||||
size_t utf16_pos = 0;
|
||||
for (size_t i = 0; i < this->utf16_to_utf8.size(); i++) {
|
||||
if (this->utf16_to_utf8[i] == pos) {
|
||||
utf16_pos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sanitize in case we get a position inside a grapheme cluster. */
|
||||
while (utf16_pos > 0 && !this->str_info[utf16_pos].char_stop) utf16_pos--;
|
||||
this->cur_pos = utf16_pos;
|
||||
|
||||
return this->utf16_to_utf8[this->cur_pos];
|
||||
}
|
||||
|
||||
/* virtual */ size_t UniscribeStringIterator::Next(IterType what)
|
||||
{
|
||||
assert(this->cur_pos <= this->utf16_to_utf8.size());
|
||||
assert(what == StringIterator::ITER_CHARACTER || what == StringIterator::ITER_WORD);
|
||||
|
||||
if (this->cur_pos == this->utf16_to_utf8.size()) return END;
|
||||
|
||||
do {
|
||||
this->cur_pos++;
|
||||
} while (this->cur_pos < this->utf16_to_utf8.size() && (what == ITER_WORD ? !this->str_info[this->cur_pos].word_stop : !this->str_info[this->cur_pos].char_stop));
|
||||
|
||||
return this->cur_pos == this->utf16_to_utf8.size() ? END : this->utf16_to_utf8[this->cur_pos];
|
||||
}
|
||||
|
||||
/*virtual */ size_t UniscribeStringIterator::Prev(IterType what)
|
||||
{
|
||||
assert(this->cur_pos <= this->utf16_to_utf8.size());
|
||||
assert(what == StringIterator::ITER_CHARACTER || what == StringIterator::ITER_WORD);
|
||||
|
||||
if (this->cur_pos == 0) return END;
|
||||
|
||||
do {
|
||||
this->cur_pos--;
|
||||
} while (this->cur_pos > 0 && (what == ITER_WORD ? !this->str_info[this->cur_pos].word_stop : !this->str_info[this->cur_pos].char_stop));
|
||||
|
||||
return this->utf16_to_utf8[this->cur_pos];
|
||||
}
|
||||
|
||||
#endif /* defined(WITH_UNISCRIBE) */
|
92
src/os/windows/string_uniscribe.h
Normal file
92
src/os/windows/string_uniscribe.h
Normal file
@@ -0,0 +1,92 @@
|
||||
/* $Id$ */
|
||||
|
||||
/*
|
||||
* This file is part of OpenTTD.
|
||||
* OpenTTD 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, version 2.
|
||||
* OpenTTD 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 OpenTTD. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @file string_uniscribe.h Functions related to laying out text on Win32. */
|
||||
|
||||
#ifndef STRING_UNISCRIBE_H
|
||||
#define STRING_UNISCRIBE_H
|
||||
|
||||
#if defined(WITH_UNISCRIBE)
|
||||
|
||||
#include "../../gfx_layout.h"
|
||||
#include "../../string_base.h"
|
||||
#include <vector>
|
||||
|
||||
|
||||
void UniscribeResetScriptCache(FontSize size);
|
||||
|
||||
|
||||
/**
|
||||
* Helper class to construct a new #UniscribeParagraphLayout.
|
||||
*/
|
||||
class UniscribeParagraphLayoutFactory {
|
||||
public:
|
||||
/** Helper for GetLayouter, to get the right type. */
|
||||
typedef wchar_t CharType;
|
||||
/** Helper for GetLayouter, to get whether the layouter supports RTL. */
|
||||
static const bool SUPPORTS_RTL = true;
|
||||
|
||||
/**
|
||||
* Get the actual ParagraphLayout for the given buffer.
|
||||
* @param buff The begin of the buffer.
|
||||
* @param buff_end The location after the last element in the buffer.
|
||||
* @param fontMapping THe mapping of the fonts.
|
||||
* @return The ParagraphLayout instance.
|
||||
*/
|
||||
static ParagraphLayouter *GetParagraphLayout(CharType *buff, CharType *buff_end, FontMap &fontMapping);
|
||||
|
||||
/**
|
||||
* Append a wide character to the internal buffer.
|
||||
* @param buff The buffer to append to.
|
||||
* @param buffer_last The end of the buffer.
|
||||
* @param c The character to add.
|
||||
* @return The number of buffer spaces that were used.
|
||||
*/
|
||||
static size_t AppendToBuffer(CharType *buff, const CharType *buffer_last, WChar c)
|
||||
{
|
||||
if (c >= 0x010000U) {
|
||||
/* Character is encoded using surrogates in UTF-16. */
|
||||
if (buff + 1 <= buffer_last) {
|
||||
buff[0] = (CharType)(((c - 0x010000U) >> 10) + 0xD800);
|
||||
buff[1] = (CharType)(((c - 0x010000U) & 0x3FF) + 0xDC00);
|
||||
} else {
|
||||
/* Not enough space in buffer. */
|
||||
*buff = 0;
|
||||
}
|
||||
return 2;
|
||||
} else {
|
||||
*buff = (CharType)(c & 0xFFFF);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** String iterator using Uniscribe as a backend. */
|
||||
class UniscribeStringIterator : public StringIterator {
|
||||
/** */
|
||||
struct CharInfo {
|
||||
bool word_stop : 1; ///< Code point is suitable as a word break.
|
||||
bool char_stop : 1; ///< Code point is the start of a grapheme cluster, i.e. a "character".
|
||||
};
|
||||
|
||||
std::vector<CharInfo> str_info; ///< Break information for each code point.
|
||||
std::vector<size_t> utf16_to_utf8; ///< Mapping from UTF-16 code point position to index in the UTF-8 source string.
|
||||
|
||||
size_t cur_pos; ///< Current iteration position.
|
||||
|
||||
public:
|
||||
virtual void SetString(const char *s);
|
||||
virtual size_t SetCurPosition(size_t pos);
|
||||
virtual size_t Next(IterType what);
|
||||
virtual size_t Prev(IterType what);
|
||||
};
|
||||
|
||||
#endif /* defined(WITH_UNISCRIBE) */
|
||||
|
||||
#endif /* STRING_UNISCRIBE_H */
|
@@ -28,6 +28,7 @@
|
||||
#include "../../crashlog.h"
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include "../../language.h"
|
||||
|
||||
/* Due to TCHAR, strncat and strncpy have to remain (for a while). */
|
||||
#include "../../safeguards.h"
|
||||
@@ -742,6 +743,70 @@ uint GetCPUCoreCount()
|
||||
return info.dwNumberOfProcessors;
|
||||
}
|
||||
|
||||
|
||||
static WCHAR _cur_iso_locale[16] = L"";
|
||||
|
||||
void Win32SetCurrentLocaleName(const char *iso_code)
|
||||
{
|
||||
/* Convert the iso code into the format that windows expects. */
|
||||
char iso[16];
|
||||
if (strcmp(iso_code, "zh_TW") == 0) {
|
||||
strecpy(iso, "zh-Hant", lastof(iso));
|
||||
} else if (strcmp(iso_code, "zh_CN") == 0) {
|
||||
strecpy(iso, "zh-Hans", lastof(iso));
|
||||
} else {
|
||||
/* Windows expects a '-' between language and country code, but we use a '_'. */
|
||||
strecpy(iso, iso_code, lastof(iso));
|
||||
for (char *c = iso; *c != '\0'; c++) {
|
||||
if (*c == '_') *c = '-';
|
||||
}
|
||||
}
|
||||
|
||||
MultiByteToWideChar(CP_UTF8, 0, iso, -1, _cur_iso_locale, lengthof(_cur_iso_locale));
|
||||
}
|
||||
|
||||
int OTTDStringCompare(const char *s1, const char *s2)
|
||||
{
|
||||
typedef int (WINAPI *PFNCOMPARESTRINGEX)(LPCWSTR, DWORD, LPCWCH, int, LPCWCH, int, LPVOID, LPVOID, LPARAM);
|
||||
static PFNCOMPARESTRINGEX _CompareStringEx = NULL;
|
||||
static bool first_time = true;
|
||||
|
||||
#ifndef SORT_DIGITSASNUMBERS
|
||||
# define SORT_DIGITSASNUMBERS 0x00000008 // use digits as numbers sort method
|
||||
#endif
|
||||
#ifndef LINGUISTIC_IGNORECASE
|
||||
# define LINGUISTIC_IGNORECASE 0x00000010 // linguistically appropriate 'ignore case'
|
||||
#endif
|
||||
|
||||
if (first_time) {
|
||||
_CompareStringEx = (PFNCOMPARESTRINGEX)GetProcAddress(GetModuleHandle(_T("Kernel32")), "CompareStringEx");
|
||||
first_time = false;
|
||||
}
|
||||
|
||||
if (_CompareStringEx != NULL) {
|
||||
/* CompareStringEx takes UTF-16 strings, even in ANSI-builds. */
|
||||
int len_s1 = MultiByteToWideChar(CP_UTF8, 0, s1, -1, NULL, 0);
|
||||
int len_s2 = MultiByteToWideChar(CP_UTF8, 0, s2, -1, NULL, 0);
|
||||
|
||||
if (len_s1 != 0 && len_s2 != 0) {
|
||||
LPWSTR str_s1 = AllocaM(WCHAR, len_s1);
|
||||
LPWSTR str_s2 = AllocaM(WCHAR, len_s2);
|
||||
|
||||
MultiByteToWideChar(CP_UTF8, 0, s1, -1, str_s1, len_s1);
|
||||
MultiByteToWideChar(CP_UTF8, 0, s2, -1, str_s2, len_s2);
|
||||
|
||||
int result = _CompareStringEx(_cur_iso_locale, LINGUISTIC_IGNORECASE | SORT_DIGITSASNUMBERS, str_s1, -1, str_s2, -1, NULL, NULL, 0);
|
||||
if (result != 0) return result;
|
||||
}
|
||||
}
|
||||
|
||||
TCHAR s1_buf[512], s2_buf[512];
|
||||
convert_to_fs(s1, s1_buf, lengthof(s1_buf));
|
||||
convert_to_fs(s2, s2_buf, lengthof(s2_buf));
|
||||
|
||||
return CompareString(MAKELCID(_current_language->winlangid, SORT_DEFAULT), NORM_IGNORECASE, s1_buf, -1, s2_buf, -1);
|
||||
}
|
||||
|
||||
#ifdef _MSC_VER
|
||||
/* Code from MSDN: https://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx */
|
||||
const DWORD MS_VC_EXCEPTION = 0x406D1388;
|
||||
|
@@ -45,4 +45,7 @@ void SetWin32ThreadName(DWORD dwThreadID, const char* threadName);
|
||||
static inline void SetWin32ThreadName(DWORD dwThreadID, const char* threadName) {}
|
||||
#endif
|
||||
|
||||
void Win32SetCurrentLocaleName(const char *iso_code);
|
||||
int OTTDStringCompare(const char *s1, const char *s2);
|
||||
|
||||
#endif /* WIN32_H */
|
||||
|
@@ -633,24 +633,28 @@ struct BuildRoadToolbarWindow : Window {
|
||||
break;
|
||||
|
||||
case DDSP_BUILD_BUSSTOP:
|
||||
PlaceRoadStop(start_tile, end_tile, (_ctrl_pressed << 5) | RoadTypeToRoadTypes(_cur_roadtype) << 2 | ROADSTOP_BUS, CMD_BUILD_ROAD_STOP | CMD_MSG(_road_type_infos[_cur_roadtype].err_build_station[ROADSTOP_BUS]));
|
||||
case DDSP_REMOVE_BUSSTOP:
|
||||
if (this->IsWidgetLowered(WID_ROT_BUS_STATION)) {
|
||||
if (_remove_button_clicked) {
|
||||
TileArea ta(start_tile, end_tile);
|
||||
DoCommandP(ta.tile, ta.w | ta.h << 8, (_ctrl_pressed << 1) | ROADSTOP_BUS, CMD_REMOVE_ROAD_STOP | CMD_MSG(_road_type_infos[_cur_roadtype].err_remove_station[ROADSTOP_BUS]), CcPlaySound_SPLAT_OTHER);
|
||||
} else {
|
||||
PlaceRoadStop(start_tile, end_tile, (_ctrl_pressed << 5) | RoadTypeToRoadTypes(_cur_roadtype) << 2 | ROADSTOP_BUS, CMD_BUILD_ROAD_STOP | CMD_MSG(_road_type_infos[_cur_roadtype].err_build_station[ROADSTOP_BUS]));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DDSP_BUILD_TRUCKSTOP:
|
||||
PlaceRoadStop(start_tile, end_tile, (_ctrl_pressed << 5) | RoadTypeToRoadTypes(_cur_roadtype) << 2 | ROADSTOP_TRUCK, CMD_BUILD_ROAD_STOP | CMD_MSG(_road_type_infos[_cur_roadtype].err_build_station[ROADSTOP_TRUCK]));
|
||||
case DDSP_REMOVE_TRUCKSTOP:
|
||||
if (this->IsWidgetLowered(WID_ROT_TRUCK_STATION)) {
|
||||
if (_remove_button_clicked) {
|
||||
TileArea ta(start_tile, end_tile);
|
||||
DoCommandP(ta.tile, ta.w | ta.h << 8, (_ctrl_pressed << 1) | ROADSTOP_TRUCK, CMD_REMOVE_ROAD_STOP | CMD_MSG(_road_type_infos[_cur_roadtype].err_remove_station[ROADSTOP_TRUCK]), CcPlaySound_SPLAT_OTHER);
|
||||
} else {
|
||||
PlaceRoadStop(start_tile, end_tile, (_ctrl_pressed << 5) | RoadTypeToRoadTypes(_cur_roadtype) << 2 | ROADSTOP_TRUCK, CMD_BUILD_ROAD_STOP | CMD_MSG(_road_type_infos[_cur_roadtype].err_build_station[ROADSTOP_TRUCK]));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DDSP_REMOVE_BUSSTOP: {
|
||||
TileArea ta(start_tile, end_tile);
|
||||
DoCommandP(ta.tile, ta.w | ta.h << 8, (_ctrl_pressed << 1) | ROADSTOP_BUS, CMD_REMOVE_ROAD_STOP | CMD_MSG(_road_type_infos[_cur_roadtype].err_remove_station[ROADSTOP_BUS]), CcPlaySound_SPLAT_OTHER);
|
||||
break;
|
||||
}
|
||||
|
||||
case DDSP_REMOVE_TRUCKSTOP: {
|
||||
TileArea ta(start_tile, end_tile);
|
||||
DoCommandP(ta.tile, ta.w | ta.h << 8, (_ctrl_pressed << 1) | ROADSTOP_TRUCK, CMD_REMOVE_ROAD_STOP | CMD_MSG(_road_type_infos[_cur_roadtype].err_remove_station[ROADSTOP_TRUCK]), CcPlaySound_SPLAT_OTHER);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -965,6 +965,8 @@ static CommandCost CheckFlatLandRoadStop(TileArea tile_area, DoCommandFlag flags
|
||||
num_roadbits += CountBits(GetRoadBits(cur_tile, ROADTYPE_ROAD));
|
||||
}
|
||||
|
||||
if (GetDisallowedRoadDirections(cur_tile) != DRD_NONE) return_cmd_error(STR_ERROR_DRIVE_THROUGH_ON_ONEWAY_ROAD);
|
||||
|
||||
/* There is a tram, check if we can build road+tram stop over it. */
|
||||
if (HasBit(cur_rts, ROADTYPE_TRAM)) {
|
||||
Owner tram_owner = GetRoadOwner(cur_tile, ROADTYPE_TRAM);
|
||||
|
@@ -25,6 +25,14 @@
|
||||
#include <errno.h> // required by vsnprintf implementation for MSVC
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
#include "os/windows/win32.h"
|
||||
#endif
|
||||
|
||||
#ifdef WITH_UNISCRIBE
|
||||
#include "os/windows/string_uniscribe.h"
|
||||
#endif
|
||||
|
||||
#ifdef WITH_ICU_SORT
|
||||
/* Required by strnatcmp. */
|
||||
#include <unicode/ustring.h>
|
||||
@@ -658,20 +666,32 @@ int strnatcmp(const char *s1, const char *s2, bool ignore_garbage_at_front)
|
||||
s1 = SkipGarbage(s1);
|
||||
s2 = SkipGarbage(s2);
|
||||
}
|
||||
|
||||
#ifdef WITH_ICU_SORT
|
||||
if (_current_collator != NULL) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
int result = _current_collator->compareUTF8(s1, s2, status);
|
||||
if (U_SUCCESS(status)) return result;
|
||||
}
|
||||
|
||||
#endif /* WITH_ICU_SORT */
|
||||
|
||||
#if defined(WIN32) && !defined(STRGEN) && !defined(SETTINGSGEN)
|
||||
int res = OTTDStringCompare(s1, s2);
|
||||
if (res != 0) return res - 2; // Convert to normal C return values.
|
||||
#endif
|
||||
|
||||
/* Do a manual natural sort comparison if ICU is missing or if we cannot create a collator. */
|
||||
return _strnatcmpIntl(s1, s2);
|
||||
}
|
||||
|
||||
#ifdef WITH_ICU_SORT
|
||||
#ifdef WITH_UNISCRIBE
|
||||
|
||||
/* static */ StringIterator *StringIterator::Create()
|
||||
{
|
||||
return new UniscribeStringIterator();
|
||||
}
|
||||
|
||||
#elif defined(WITH_ICU_SORT)
|
||||
|
||||
#include <unicode/utext.h>
|
||||
#include <unicode/brkiter.h>
|
||||
|
@@ -2048,6 +2048,11 @@ bool ReadLanguagePack(const LanguageMetadata *lang)
|
||||
strecpy(_config_language_file, c_file, lastof(_config_language_file));
|
||||
SetCurrentGrfLangID(_current_language->newgrflangid);
|
||||
|
||||
#ifdef WIN32
|
||||
extern void Win32SetCurrentLocaleName(const char *iso_code);
|
||||
Win32SetCurrentLocaleName(_current_language->isocode);
|
||||
#endif
|
||||
|
||||
#ifdef WITH_ICU_SORT
|
||||
/* Delete previous collator. */
|
||||
if (_current_collator != NULL) {
|
||||
@@ -2390,7 +2395,7 @@ void CheckForMissingGlyphs(bool base_font, MissingGlyphSearcher *searcher)
|
||||
/* Update the font with cache */
|
||||
LoadStringWidthTable(searcher->Monospace());
|
||||
|
||||
#if !defined(WITH_ICU_LAYOUT)
|
||||
#if !defined(WITH_ICU_LAYOUT) && !defined(WITH_UNISCRIBE)
|
||||
/*
|
||||
* For right-to-left languages we need the ICU library. If
|
||||
* we do not have support for that library we warn the user
|
||||
|
@@ -343,13 +343,15 @@ public:
|
||||
SetDParam(1, this->town->cache.num_houses);
|
||||
DrawString(r.left + WD_FRAMERECT_LEFT, r.right - WD_FRAMERECT_LEFT, y, STR_TOWN_VIEW_POPULATION_HOUSES);
|
||||
|
||||
SetDParam(0, this->town->supplied[CT_PASSENGERS].old_act);
|
||||
SetDParam(1, this->town->supplied[CT_PASSENGERS].old_max);
|
||||
DrawString(r.left + WD_FRAMERECT_LEFT, r.right - WD_FRAMERECT_LEFT, y += FONT_HEIGHT_NORMAL, STR_TOWN_VIEW_PASSENGERS_LAST_MONTH_MAX);
|
||||
SetDParam(0, 1 << CT_PASSENGERS);
|
||||
SetDParam(1, this->town->supplied[CT_PASSENGERS].old_act);
|
||||
SetDParam(2, this->town->supplied[CT_PASSENGERS].old_max);
|
||||
DrawString(r.left + WD_FRAMERECT_LEFT, r.right - WD_FRAMERECT_LEFT, y += FONT_HEIGHT_NORMAL, STR_TOWN_VIEW_CARGO_LAST_MONTH_MAX);
|
||||
|
||||
SetDParam(0, this->town->supplied[CT_MAIL].old_act);
|
||||
SetDParam(1, this->town->supplied[CT_MAIL].old_max);
|
||||
DrawString(r.left + WD_FRAMERECT_LEFT, r.right - WD_FRAMERECT_LEFT, y += FONT_HEIGHT_NORMAL, STR_TOWN_VIEW_MAIL_LAST_MONTH_MAX);
|
||||
SetDParam(0, 1 << CT_MAIL);
|
||||
SetDParam(1, this->town->supplied[CT_MAIL].old_act);
|
||||
SetDParam(2, this->town->supplied[CT_MAIL].old_max);
|
||||
DrawString(r.left + WD_FRAMERECT_LEFT, r.right - WD_FRAMERECT_LEFT, y += FONT_HEIGHT_NORMAL, STR_TOWN_VIEW_CARGO_LAST_MONTH_MAX);
|
||||
|
||||
bool first = true;
|
||||
for (int i = TE_BEGIN; i < TE_END; i++) {
|
||||
|
Reference in New Issue
Block a user