mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-03-22 15:05:03 +01:00
build 180909. scenario time override, support for random texture sets in .mat files, minor bug fixes
This commit is contained in:
@@ -2817,9 +2817,9 @@ bool TController::IncSpeed()
|
||||
// if it generates enough traction force
|
||||
// to build up speed to 30/40 km/h for passenger/cargo train (10 km/h less if going uphill)
|
||||
auto const sufficienttractionforce { std::abs( mvControlling->Ft ) > ( IsHeavyCargoTrain ? 125 : 100 ) * 1000.0 };
|
||||
auto const useseriesmodevoltage { 0.80 * mvControlling->EnginePowerSource.CollectorParameters.MaxV };
|
||||
auto const seriesmodefieldshunting { ( mvControlling->ScndCtrlPos > 0 ) && ( mvControlling->RList[ mvControlling->MainCtrlPos ].Bn == 1 ) };
|
||||
auto const parallelmodefieldshunting { ( mvControlling->ScndCtrlPos > 0 ) && ( mvControlling->RList[ mvControlling->MainCtrlPos ].Bn > 1 ) };
|
||||
auto const useseriesmodevoltage { 0.80 * mvControlling->EnginePowerSource.CollectorParameters.MaxV };
|
||||
auto const useseriesmode = (
|
||||
( mvControlling->Imax > mvControlling->ImaxLo )
|
||||
|| ( fVoltage < useseriesmodevoltage )
|
||||
|
||||
@@ -307,6 +307,12 @@ global_settings::ConfigParse(cParser &Parser) {
|
||||
// max 8 lights per opengl specs, minus one used for sun. at least one light for controlled vehicle
|
||||
DynamicLightCount = clamp( DynamicLightCount, 1, 7 );
|
||||
}
|
||||
else if( token == "scenario.time.override" ) {
|
||||
// shift (in hours) applied to train timetables
|
||||
Parser.getTokens( 1, false );
|
||||
Parser >> ScenarioTimeOverride;
|
||||
ScenarioTimeOverride = clamp( ScenarioTimeOverride, 0.f, 24 * 1439 / 1440.f );
|
||||
}
|
||||
else if( token == "scenario.time.offset" ) {
|
||||
// shift (in hours) applied to train timetables
|
||||
Parser.getTokens( 1, false );
|
||||
|
||||
@@ -78,6 +78,7 @@ struct global_settings {
|
||||
bool FakeLight{ false }; // toggle between fixed and dynamic daylight
|
||||
double fTimeSpeed{ 1.0 }; // przyspieszenie czasu, zmienna do testów
|
||||
double fLatitudeDeg{ 52.0 }; // szerokość geograficzna
|
||||
float ScenarioTimeOverride { std::numeric_limits<float>::quiet_NaN() }; // requested scenario start time
|
||||
float ScenarioTimeOffset { 0.f }; // time shift (in hours) applied to train timetables
|
||||
bool ScenarioTimeCurrent { false }; // automatic time shift to match scenario time with local clock
|
||||
bool bInactivePause{ true }; // automatyczna pauza, gdy okno nieaktywne
|
||||
|
||||
@@ -1127,7 +1127,8 @@ public:
|
||||
double dizel_engage = 0.0; /*sprzeglo skrzyni biegow: aktualny docisk*/
|
||||
double dizel_automaticgearstatus = 0.0; /*0 - bez zmiany, -1 zmiana na nizszy +1 zmiana na wyzszy*/
|
||||
bool dizel_startup { false }; // engine startup procedure request indicator
|
||||
bool dizel_ignition = false; // engine ignition request indicator
|
||||
bool dizel_ignition { false }; // engine ignition request indicator
|
||||
bool dizel_spinup { false }; // engine spin up to idle speed flag
|
||||
double dizel_engagedeltaomega = 0.0; /*roznica predkosci katowych tarcz sprzegla*/
|
||||
double dizel_n_old = 0.0; /*poredkosc na potrzeby obliczen sprzegiel*/
|
||||
double dizel_Torque = 0.0; /*poredkosc na potrzeby obliczen sprzegiel*/
|
||||
|
||||
@@ -5974,8 +5974,9 @@ bool TMoverParameters::dizel_Update(double dt) {
|
||||
dizel_startup = false;
|
||||
dizel_ignition = false;
|
||||
// TODO: split engine and main circuit state indicator in two separate flags
|
||||
Mains = true;
|
||||
LastSwitchingTime = 0;
|
||||
Mains = true;
|
||||
dizel_spinup = true;
|
||||
enrot = std::max(
|
||||
enrot,
|
||||
0.35 * ( // TODO: dac zaleznie od temperatury i baterii
|
||||
@@ -5985,6 +5986,14 @@ bool TMoverParameters::dizel_Update(double dt) {
|
||||
|
||||
}
|
||||
|
||||
dizel_spinup = (
|
||||
dizel_spinup
|
||||
&& Mains
|
||||
&& ( enrot < 0.95 * (
|
||||
EngineType == TEngineType::DieselEngine ?
|
||||
dizel_nmin :
|
||||
DElist[ 0 ].RPM / 60.0 ) ) );
|
||||
|
||||
if( ( true == Mains )
|
||||
&& ( false == FuelPump.is_active ) ) {
|
||||
// knock out the engine if the fuel pump isn't feeding it
|
||||
@@ -6105,7 +6114,7 @@ double TMoverParameters::dizel_Momentum(double dizel_fill, double n, double dt)
|
||||
// wstrzymywanie przy malych obrotach
|
||||
Moment -= dizel_Mstand;
|
||||
}
|
||||
if (true == dizel_ignition)
|
||||
if (true == dizel_spinup)
|
||||
Moment += dizel_Mstand / (0.3 + std::max(0.0, enrot/dizel_nmin)); //rozrusznik
|
||||
|
||||
dizel_Torque = Moment;
|
||||
@@ -6216,11 +6225,10 @@ double TMoverParameters::dizel_Momentum(double dizel_fill, double n, double dt)
|
||||
}
|
||||
|
||||
|
||||
if ((enrot <= 0) && (!dizel_ignition))
|
||||
{
|
||||
Mains = false;
|
||||
enrot = 0;
|
||||
}
|
||||
if( ( enrot <= 0 ) && ( false == dizel_spinup ) ) {
|
||||
MainSwitch( false );
|
||||
enrot = 0;
|
||||
}
|
||||
|
||||
dizel_n_old = n; //obecna predkosc katowa na potrzeby kolejnej klatki
|
||||
|
||||
|
||||
33
Train.cpp
33
Train.cpp
@@ -5094,16 +5094,16 @@ bool TTrain::Update( double const Deltatime )
|
||||
( true == mvControlled->ResistorsFlagCheck() )
|
||||
|| ( mvControlled->MainCtrlActualPos == 0 ) ); // do EU04
|
||||
|
||||
if( ( mvControlled->Im != 0 )
|
||||
|| ( mvOccupied->BrakePress > 2 )
|
||||
if( ( mvControlled->StLinFlag )
|
||||
|| ( mvOccupied->BrakePress > 2.0 )
|
||||
|| ( mvOccupied->PipePress < 3.6 ) ) {
|
||||
// Ra: czy to jest udawanie działania styczników liniowych?
|
||||
btLampkaStyczn.Turn( false );
|
||||
}
|
||||
else if( mvOccupied->BrakePress < 1 )
|
||||
else if( mvOccupied->BrakePress < 1.0 )
|
||||
btLampkaStyczn.Turn( true ); // mozna prowadzic rozruch
|
||||
if( ( ( TestFlag( mvControlled->Couplers[ 1 ].CouplingFlag, ctrain_controll ) ) && ( mvControlled->CabNo == 1 ) ) ||
|
||||
( ( TestFlag( mvControlled->Couplers[ 0 ].CouplingFlag, ctrain_controll ) ) && ( mvControlled->CabNo == -1 ) ) )
|
||||
if( ( ( TestFlag( mvControlled->Couplers[ side::rear ].CouplingFlag, coupling::control ) ) && ( mvControlled->CabNo == 1 ) )
|
||||
|| ( ( TestFlag( mvControlled->Couplers[ side::front ].CouplingFlag, coupling::control ) ) && ( mvControlled->CabNo == -1 ) ) )
|
||||
btLampkaUkrotnienie.Turn( true );
|
||||
else
|
||||
btLampkaUkrotnienie.Turn( false );
|
||||
@@ -5155,10 +5155,10 @@ bool TTrain::Update( double const Deltatime )
|
||||
}
|
||||
|
||||
if( mvControlled->Signalling == true ) {
|
||||
if( mvOccupied->BrakePress >= 0.145f ) {
|
||||
if( mvOccupied->BrakePress >= 1.45f ) {
|
||||
btLampkaHamowanie1zes.Turn( true );
|
||||
}
|
||||
if( mvControlled->BrakePress < 0.075f ) {
|
||||
if( mvControlled->BrakePress < 0.75f ) {
|
||||
btLampkaHamowanie1zes.Turn( false );
|
||||
}
|
||||
}
|
||||
@@ -5298,12 +5298,12 @@ bool TTrain::Update( double const Deltatime )
|
||||
( true == tmp->MoverParameters->ResistorsFlagCheck() )
|
||||
|| ( tmp->MoverParameters->MainCtrlActualPos == 0 ) ); // do EU04
|
||||
|
||||
if( ( tmp->MoverParameters->Itot != 0 )
|
||||
|| ( tmp->MoverParameters->BrakePress > 0.2 )
|
||||
if( ( tmp->MoverParameters->StLinFlag )
|
||||
|| ( tmp->MoverParameters->BrakePress > 2.0 )
|
||||
|| ( tmp->MoverParameters->PipePress < 0.36 ) ) {
|
||||
btLampkaStycznB.Turn( false );
|
||||
}
|
||||
else if( tmp->MoverParameters->BrakePress < 0.1 ) {
|
||||
else if( tmp->MoverParameters->BrakePress < 1.0 ) {
|
||||
btLampkaStycznB.Turn( true ); // mozna prowadzic rozruch
|
||||
}
|
||||
// hunter-271211: sygnalizacja poslizgu w pierwszym pojezdzie, gdy wystapi w drugim
|
||||
@@ -5311,12 +5311,15 @@ bool TTrain::Update( double const Deltatime )
|
||||
|
||||
btLampkaSprezarkaB.Turn( tmp->MoverParameters->CompressorFlag ); // mutopsitka dziala
|
||||
btLampkaSprezarkaBOff.Turn( false == tmp->MoverParameters->CompressorFlag );
|
||||
if ((tmp->MoverParameters->BrakePress >= 0.145f) && (mvControlled->Signalling == true))
|
||||
{
|
||||
btLampkaHamowanie2zes.Turn( true );
|
||||
if( mvControlled->Signalling == true ) {
|
||||
if( tmp->MoverParameters->BrakePress >= 1.45f ) {
|
||||
btLampkaHamowanie2zes.Turn( true );
|
||||
}
|
||||
if( tmp->MoverParameters->BrakePress < 0.75f ) {
|
||||
btLampkaHamowanie2zes.Turn( false );
|
||||
}
|
||||
}
|
||||
if ((tmp->MoverParameters->BrakePress < 0.075f) || (mvControlled->Signalling == false))
|
||||
{
|
||||
else {
|
||||
btLampkaHamowanie2zes.Turn( false );
|
||||
}
|
||||
btLampkaNadmPrzetwB.Turn( tmp->MoverParameters->ConvOvldFlag ); // nadmiarowy przetwornicy?
|
||||
|
||||
27
material.cpp
27
material.cpp
@@ -33,13 +33,10 @@ opengl_material::deserialize( cParser &Input, bool const Loadnow ) {
|
||||
// imports member data pair from the config file
|
||||
bool
|
||||
opengl_material::deserialize_mapping( cParser &Input, int const Priority, bool const Loadnow ) {
|
||||
// token can be a key or block end
|
||||
std::string const key { Input.getToken<std::string>( true, "\n\r\t ;[]" ) };
|
||||
|
||||
if( ( true == key.empty() ) || ( key == "}" ) ) { return false; }
|
||||
|
||||
// NOTE: comma can be part of legacy file names, so we don't treat it as a separator here
|
||||
auto const value { Input.getToken<std::string>( true, "\n\r\t ;" ) };
|
||||
std::string const key { Input.getToken<std::string>( true, "\n\r\t ;[]" ) };
|
||||
// key can be an actual key or block end
|
||||
if( ( true == key.empty() ) || ( key == "}" ) ) { return false; }
|
||||
|
||||
if( Priority != -1 ) {
|
||||
// regular attribute processing mode
|
||||
@@ -60,7 +57,7 @@ opengl_material::deserialize_mapping( cParser &Input, int const Priority, bool c
|
||||
|| ( key == "texture_diffuse:" ) ) {
|
||||
if( ( texture1 == null_handle )
|
||||
|| ( Priority > priority1 ) ) {
|
||||
texture1 = GfxRenderer.Fetch_Texture( value, Loadnow );
|
||||
texture1 = GfxRenderer.Fetch_Texture( deserialize_random_set( Input ), Loadnow );
|
||||
priority1 = Priority;
|
||||
}
|
||||
}
|
||||
@@ -68,20 +65,24 @@ opengl_material::deserialize_mapping( cParser &Input, int const Priority, bool c
|
||||
|| ( key == "texture_normalmap:" ) ) {
|
||||
if( ( texture2 == null_handle )
|
||||
|| ( Priority > priority2 ) ) {
|
||||
texture2 = GfxRenderer.Fetch_Texture( value, Loadnow );
|
||||
texture2 = GfxRenderer.Fetch_Texture( deserialize_random_set( Input ), Loadnow );
|
||||
priority2 = Priority;
|
||||
}
|
||||
}
|
||||
else if( value == "{" ) {
|
||||
// unrecognized or ignored token, but comes with attribute block and potential further nesting
|
||||
// go through it and discard the content
|
||||
while( true == deserialize_mapping( Input, -1, Loadnow ) ) {
|
||||
; // all work is done in the header
|
||||
else {
|
||||
auto const value { Input.getToken<std::string>( true, "\n\r\t ;" ) };
|
||||
if( value == "{" ) {
|
||||
// unrecognized or ignored token, but comes with attribute block and potential further nesting
|
||||
// go through it and discard the content
|
||||
while( true == deserialize_mapping( Input, -1, Loadnow ) ) {
|
||||
; // all work is done in the header
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// discard mode; ignores all retrieved tokens
|
||||
auto const value { Input.getToken<std::string>( true, "\n\r\t ;" ) };
|
||||
if( value == "{" ) {
|
||||
// ignored tokens can come with their own blocks, ignore these recursively
|
||||
// go through it and discard the content
|
||||
|
||||
@@ -37,6 +37,9 @@ private:
|
||||
// imports member data pair from the config file, overriding existing parameter values of lower priority
|
||||
bool
|
||||
deserialize_mapping( cParser &Input, int const Priority, bool const Loadnow );
|
||||
// extracts name of the sound file from provided data stream
|
||||
std::string
|
||||
deserialize_filename( cParser &Input );
|
||||
|
||||
// members
|
||||
int priority1 { -1 }; // priority of last loaded primary texture
|
||||
|
||||
@@ -541,6 +541,10 @@ state_serializer::deserialize_time( cParser &Input, scene::scratch_data &Scratch
|
||||
auto const *localtime = std::localtime( &timenow );
|
||||
Global.ScenarioTimeOffset = ( ( localtime->tm_hour * 60 + localtime->tm_min ) - ( time.wHour * 60 + time.wMinute ) ) / 60.f;
|
||||
}
|
||||
else if( false == std::isnan( Global.ScenarioTimeOverride ) ) {
|
||||
// scenario time override takes precedence over scenario time offset
|
||||
Global.ScenarioTimeOffset = ( ( Global.ScenarioTimeOverride * 60 ) - ( time.wHour * 60 + time.wMinute ) ) / 60.f;
|
||||
}
|
||||
|
||||
// remaining sunrise and sunset parameters are no longer used, as they're now calculated dynamically
|
||||
// anything else left in the section has no defined meaning
|
||||
|
||||
64
sound.cpp
64
sound.cpp
@@ -101,13 +101,13 @@ sound_source::deserialize( cParser &Input, sound_type const Legacytype, int cons
|
||||
switch( Legacytype ) {
|
||||
case sound_type::single: {
|
||||
// single sample only
|
||||
m_sounds[ main ].buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) );
|
||||
m_sounds[ main ].buffer = audio::renderer.fetch_buffer( deserialize_random_set( Input, "\n\r\t ,;" ) );
|
||||
break;
|
||||
}
|
||||
case sound_type::multipart: {
|
||||
// three samples: start, middle, stop
|
||||
for( auto &sound : m_sounds ) {
|
||||
sound.buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) );
|
||||
sound.buffer = audio::renderer.fetch_buffer( deserialize_random_set( Input, "\n\r\t ,;" ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -147,32 +147,6 @@ sound_source::deserialize( cParser &Input, sound_type const Legacytype, int cons
|
||||
return *this;
|
||||
}
|
||||
|
||||
// extracts name of the sound file from provided data stream
|
||||
std::string
|
||||
sound_source::deserialize_filename( cParser &Input ) {
|
||||
|
||||
auto token { Input.getToken<std::string>( true, "\n\r\t ,;" ) };
|
||||
if( token != "[" ) {
|
||||
// simple case, single file
|
||||
return token;
|
||||
}
|
||||
// if instead of filename we've encountered '[' this marks a beginning of random sounds
|
||||
// we retrieve all entries, then return a random one
|
||||
std::vector<std::string> filenames;
|
||||
while( ( ( token = Input.getToken<std::string>( true, "\n\r\t ,;" ) ) != "" )
|
||||
&& ( token != "]" ) ) {
|
||||
filenames.emplace_back( token );
|
||||
}
|
||||
if( false == filenames.empty() ) {
|
||||
std::shuffle( std::begin( filenames ), std::end( filenames ), Global.random_engine );
|
||||
return filenames.front();
|
||||
}
|
||||
else {
|
||||
// shouldn't ever get here but, eh
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// imports member data pair from the config file
|
||||
bool
|
||||
sound_source::deserialize_mapping( cParser &Input ) {
|
||||
@@ -183,16 +157,16 @@ sound_source::deserialize_mapping( cParser &Input ) {
|
||||
|
||||
// if not block end then the key is followed by assigned value or sub-block
|
||||
if( key == "soundmain:" ) {
|
||||
sound( sound_id::main ).buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) );
|
||||
sound( sound_id::main ).buffer = audio::renderer.fetch_buffer( deserialize_random_set( Input, "\n\r\t ,;" ) );
|
||||
}
|
||||
else if( key == "soundset:" ) {
|
||||
deserialize_soundset( Input );
|
||||
}
|
||||
else if( key == "soundbegin:" ) {
|
||||
sound( sound_id::begin ).buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) );
|
||||
sound( sound_id::begin ).buffer = audio::renderer.fetch_buffer( deserialize_random_set( Input, "\n\r\t ,;" ) );
|
||||
}
|
||||
else if( key == "soundend:" ) {
|
||||
sound( sound_id::end ).buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) );
|
||||
sound( sound_id::end ).buffer = audio::renderer.fetch_buffer( deserialize_random_set( Input, "\n\r\t ,;" ) );
|
||||
}
|
||||
else if( key.compare( 0, std::min<std::size_t>( key.size(), 5 ), "sound" ) == 0 ) {
|
||||
// sound chunks, defined with key soundX where X = activation threshold
|
||||
@@ -203,7 +177,7 @@ sound_source::deserialize_mapping( cParser &Input ) {
|
||||
m_soundchunks.emplace_back(
|
||||
soundchunk_pair {
|
||||
// sound data
|
||||
{ audio::renderer.fetch_buffer( deserialize_filename( Input ) ), 0 },
|
||||
{ audio::renderer.fetch_buffer( deserialize_random_set( Input, "\n\r\t ,;" ) ), 0 },
|
||||
// chunk data
|
||||
{ std::stoi( key.substr( indexstart, indexend - indexstart ) ), 0, 0, 1.f } } );
|
||||
}
|
||||
@@ -272,26 +246,12 @@ sound_source::deserialize_mapping( cParser &Input ) {
|
||||
void
|
||||
sound_source::deserialize_soundset( cParser &Input ) {
|
||||
|
||||
auto token { Input.getToken<std::string>( true, "\n\r\t ,;|" ) };
|
||||
if( token != "[" ) {
|
||||
// simple case, basic set of three filenames separated with |
|
||||
// three samples: start, middle, stop
|
||||
sound( sound_id::begin ).buffer = audio::renderer.fetch_buffer( token );
|
||||
sound( sound_id::main ).buffer = audio::renderer.fetch_buffer( Input.getToken<std::string>( true, "\n\r\t ,;|" ) );
|
||||
sound( sound_id::end ).buffer = audio::renderer.fetch_buffer( Input.getToken<std::string>( true, "\n\r\t ,;|" ) );
|
||||
return;
|
||||
}
|
||||
// if instead of filename we've encountered '[' this marks a beginning of random sets
|
||||
// we retrieve all entries, then process a random one
|
||||
std::vector<std::string> soundsets;
|
||||
while( ( ( token = Input.getToken<std::string>( true, "\n\r\t ,;" ) ) != "" )
|
||||
&& ( token != "]" ) ) {
|
||||
soundsets.emplace_back( token );
|
||||
}
|
||||
if( false == soundsets.empty() ) {
|
||||
std::shuffle( std::begin( soundsets ), std::end( soundsets ), Global.random_engine );
|
||||
return deserialize_soundset( cParser( soundsets.front() ) );
|
||||
}
|
||||
auto const soundset { deserialize_random_set( Input, "\n\r\t ,;" ) };
|
||||
// split retrieved set
|
||||
cParser setparser( soundset );
|
||||
sound( sound_id::begin ).buffer = audio::renderer.fetch_buffer( setparser.getToken<std::string>( true, "|" ) );
|
||||
sound( sound_id::main ).buffer = audio::renderer.fetch_buffer( setparser.getToken<std::string>( true, "|" ) );
|
||||
sound( sound_id::end ).buffer = audio::renderer.fetch_buffer( setparser.getToken<std::string>( true, "|" ) );
|
||||
}
|
||||
|
||||
// sends content of the class in legacy (text) format to provided stream
|
||||
|
||||
3
sound.h
3
sound.h
@@ -150,9 +150,6 @@ private:
|
||||
};
|
||||
|
||||
// methods
|
||||
// extracts name of the sound file from provided data stream
|
||||
std::string
|
||||
deserialize_filename( cParser &Input );
|
||||
// imports member data pair from the provided data stream
|
||||
bool
|
||||
deserialize_mapping( cParser &Input );
|
||||
|
||||
@@ -416,3 +416,29 @@ glm::dvec3 LoadPoint( cParser &Input ) {
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
// extracts a group of tokens from provided data stream, returns one of them picked randomly
|
||||
std::string
|
||||
deserialize_random_set( cParser &Input, char const *Break ) {
|
||||
|
||||
auto token { Input.getToken<std::string>( true, Break ) };
|
||||
if( token != "[" ) {
|
||||
// simple case, single token
|
||||
return token;
|
||||
}
|
||||
// if instead of a single token we've encountered '[' this marks a beginning of a random set
|
||||
// we retrieve all entries, then return a random one
|
||||
std::vector<std::string> tokens;
|
||||
while( ( ( token = deserialize_random_set( Input, Break ) ) != "" )
|
||||
&& ( token != "]" ) ) {
|
||||
tokens.emplace_back( token );
|
||||
}
|
||||
if( false == tokens.empty() ) {
|
||||
std::shuffle( std::begin( tokens ), std::end( tokens ), Global.random_engine );
|
||||
return tokens.front();
|
||||
}
|
||||
else {
|
||||
// shouldn't ever get here but, eh
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,4 +309,8 @@ nearest_segment_point( VecType_ const &Segmentstart, VecType_ const &Segmentend,
|
||||
|
||||
glm::dvec3 LoadPoint( class cParser &Input );
|
||||
|
||||
// extracts a group of tokens from provided data stream
|
||||
std::string
|
||||
deserialize_random_set( cParser &Input, char const *Break = "\n\r\t ;" );
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user