mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-22 23:19:19 +02:00
engine-powered compressor sound pitch adjustment, model node angles, global settings export and diagnostics, departure permit sound enhancement, radio channel selection ai logic enhancement, door permission ai logic fix
This commit is contained in:
@@ -432,6 +432,15 @@ bool TAnimModel::Init(std::string const &asName, std::string const &asReplacable
|
||||
return ( pModel != nullptr );
|
||||
}
|
||||
|
||||
bool
|
||||
TAnimModel::is_keyword( std::string const &Token ) const {
|
||||
|
||||
return ( Token == "endmodel" )
|
||||
|| ( Token == "lights" )
|
||||
|| ( Token == "lightcolors" )
|
||||
|| ( Token == "angles" );
|
||||
}
|
||||
|
||||
bool TAnimModel::Load(cParser *parser, bool ter)
|
||||
{ // rozpoznanie wpisu modelu i ustawienie świateł
|
||||
std::string name = parser->getToken<std::string>();
|
||||
@@ -485,8 +494,7 @@ bool TAnimModel::Load(cParser *parser, bool ter)
|
||||
if( token == "lights" ) {
|
||||
auto i{ 0 };
|
||||
while( ( false == ( token = parser->getToken<std::string>() ).empty() )
|
||||
&& ( token != "lightcolors" )
|
||||
&& ( token != "endmodel" ) ) {
|
||||
&& ( false == is_keyword( token ) ) ) {
|
||||
|
||||
if( i < iNumLights ) {
|
||||
// stan światła jest liczbą z ułamkiem
|
||||
@@ -499,8 +507,7 @@ bool TAnimModel::Load(cParser *parser, bool ter)
|
||||
if( token == "lightcolors" ) {
|
||||
auto i{ 0 };
|
||||
while( ( false == ( token = parser->getToken<std::string>() ).empty() )
|
||||
&& ( token != "lights" )
|
||||
&& ( token != "endmodel" ) ) {
|
||||
&& ( false == is_keyword( token ) ) ) {
|
||||
|
||||
if( ( i < iNumLights )
|
||||
&& ( token != "-1" ) ) { // -1 leaves the default color intact
|
||||
@@ -513,6 +520,15 @@ bool TAnimModel::Load(cParser *parser, bool ter)
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
if( token == "angles" ) {
|
||||
parser->getTokens( 3 );
|
||||
*parser
|
||||
>> vAngle[ 0 ]
|
||||
>> vAngle[ 1 ]
|
||||
>> vAngle[ 2 ];
|
||||
}
|
||||
|
||||
} while( ( false == token.empty() )
|
||||
&& ( token != "endmodel" ) );
|
||||
|
||||
|
||||
@@ -177,6 +177,8 @@ private:
|
||||
void deserialize_( std::istream &Input );
|
||||
// export() subclass details, sends basic content of the class in legacy (text) format to provided stream
|
||||
void export_as_text_( std::ostream &Output ) const;
|
||||
// checks whether provided token is a legacy (text) format keyword
|
||||
bool is_keyword( std::string const &Token ) const;
|
||||
|
||||
// members
|
||||
TAnimContainer *pRoot { nullptr }; // pojemniki sterujące, tylko dla aniomowanych submodeli
|
||||
|
||||
75
Driver.cpp
75
Driver.cpp
@@ -898,6 +898,21 @@ TCommandType TController::TableUpdate(double &fVelDes, double &fDist, double &fN
|
||||
}
|
||||
else if (iDrivigFlags & moveStopPoint) // jeśli pomijanie W4, to nie sprawdza czasu odjazdu
|
||||
{ // tylko gdy nazwa zatrzymania się zgadza
|
||||
if( ( OrderCurrentGet() & ( Obey_train | Bank ) ) != 0 ) {
|
||||
// check whether the station specifies radio channel change
|
||||
// NOTE: we don't do it in shunt mode, as shunting operations tend to use dedicated radio channel
|
||||
// NOTE: there's a risk radio channel change was specified by a station which we skipped during timetable rewind
|
||||
// we ignore this for the time being as it's not a high priority error
|
||||
auto const radiochannel { TrainParams.radio_channel() };
|
||||
if( radiochannel > 0 ) {
|
||||
if( iGuardRadio != 0 ) {
|
||||
iGuardRadio = radiochannel;
|
||||
}
|
||||
if( AIControllFlag ) {
|
||||
iRadioChannel = radiochannel;
|
||||
}
|
||||
}
|
||||
}
|
||||
IsScheduledPassengerStopVisible = true; // block potential timetable rewind if the next stop shows up later in the scan
|
||||
if (false == TrainParams.IsStop())
|
||||
{ // jeśli nie ma tu postoju
|
||||
@@ -3217,8 +3232,8 @@ bool TController::IncSpeed()
|
||||
return false;
|
||||
}
|
||||
bool OK = true;
|
||||
if( ( doors_open() )
|
||||
&& ( VelDesired > 0.0 ) ) { // to prevent door shuffle on stop
|
||||
if( ( VelDesired > 0.0 ) // to prevent door shuffle on stop
|
||||
&& ( doors_open() || doors_permit_active() ) ) {
|
||||
// zamykanie drzwi - tutaj wykonuje tylko AI (zmienia fActionTime)
|
||||
Doors( false );
|
||||
}
|
||||
@@ -4091,14 +4106,14 @@ void TController::Doors( bool const Open, int const Side ) {
|
||||
}
|
||||
else {
|
||||
// zamykanie
|
||||
if( ( false == pVehicle->MoverParameters->Doors.permit_needed )
|
||||
if( ( false == doors_permit_active() )
|
||||
&& ( false == doors_open() ) ) {
|
||||
// the doors are already closed and we don't have to revoke control permit, we can skip all hard work
|
||||
iDrivigFlags &= ~moveDoorOpened;
|
||||
return;
|
||||
}
|
||||
|
||||
if( AIControllFlag ) {
|
||||
if( ( AIControllFlag ) && ( doors_open() ) ) {
|
||||
if( ( true == mvOccupied->Doors.has_warning )
|
||||
&& ( false == mvOccupied->DepartureSignal )
|
||||
&& ( true == TestFlag( iDrivigFlags, moveDoorOpened ) ) ) {
|
||||
@@ -4107,10 +4122,9 @@ void TController::Doors( bool const Open, int const Side ) {
|
||||
}
|
||||
}
|
||||
|
||||
if( ( true == doors_open() )
|
||||
&& ( ( fActionTime > -0.5 )
|
||||
|| ( false == AIControllFlag ) ) ) {
|
||||
if( ( false == AIControllFlag ) || ( fActionTime > -0.5 ) ) {
|
||||
// ai doesn't close the door until it's free to depart, but human driver has free reign to do stupid things
|
||||
if( true == doors_open() ) {
|
||||
if( ( pVehicle->MoverParameters->Doors.close_control == control_t::conductor )
|
||||
|| ( ( true == AIControllFlag ) ) ) {
|
||||
// if the door are controlled by the driver, we let the user operate them unless this user is an ai
|
||||
@@ -4120,7 +4134,10 @@ void TController::Doors( bool const Open, int const Side ) {
|
||||
pVehicle->MoverParameters->OperateDoors( side::right, false );
|
||||
pVehicle->MoverParameters->OperateDoors( side::left, false );
|
||||
}
|
||||
if( pVehicle->MoverParameters->Doors.permit_needed ) {
|
||||
}
|
||||
}
|
||||
if( true == doors_permit_active() ) {
|
||||
if( true == AIControllFlag ) {
|
||||
pVehicle->MoverParameters->PermitDoors( side::right, false );
|
||||
pVehicle->MoverParameters->PermitDoors( side::left, false );
|
||||
}
|
||||
@@ -4157,19 +4174,14 @@ TController::doors_open() const {
|
||||
return (
|
||||
IsAnyDoorOpen[ side::right ]
|
||||
|| IsAnyDoorOpen[ side::left ] );
|
||||
/*
|
||||
auto *vehicle = pVehicles[ 0 ]; // pojazd na czole składu
|
||||
while( vehicle != nullptr ) {
|
||||
if( ( false == vehicle->MoverParameters->Doors.instances[side::right].is_closed )
|
||||
|| ( false == vehicle->MoverParameters->Doors.instances[side::left].is_closed ) ) {
|
||||
// any open door is enough
|
||||
return true;
|
||||
}
|
||||
vehicle = vehicle->Next();
|
||||
}
|
||||
// if we're still here there's nothing open
|
||||
return false;
|
||||
*/
|
||||
|
||||
bool
|
||||
TController::doors_permit_active() const {
|
||||
|
||||
return (
|
||||
IsAnyDoorPermitActive[ side::right ]
|
||||
|| IsAnyDoorPermitActive[ side::left ] );
|
||||
}
|
||||
|
||||
void TController::RecognizeCommand()
|
||||
@@ -4275,7 +4287,7 @@ bool TController::PutCommand( std::string NewCommand, double NewValue1, double N
|
||||
{ ".ogg", ".flac", ".wav" } );
|
||||
if( false == lookup.first.empty() ) {
|
||||
// wczytanie dźwięku odjazdu w wersji radiowej (słychać tylko w kabinie)
|
||||
tsGuardSignal = sound_source( sound_placement::internal, 2 * EU07_SOUND_CABCONTROLSCUTOFFRANGE ).deserialize( lookup.first + lookup.second, sound_type::single );
|
||||
tsGuardSignal = sound_source( sound_placement::internal, EU07_SOUND_HANDHELDRADIORANGE ).deserialize( lookup.first + lookup.second, sound_type::single );
|
||||
iGuardRadio = iRadioChannel;
|
||||
}
|
||||
}
|
||||
@@ -4733,6 +4745,7 @@ TController::UpdateSituation(double dt) {
|
||||
double AbsAccS = 0;
|
||||
IsAnyCouplerStretched = false;
|
||||
IsAnyDoorOpen[ side::right ] = IsAnyDoorOpen[ side::left ] = false;
|
||||
IsAnyDoorPermitActive[ side::right ] = IsAnyDoorPermitActive[ side::left ] = false;
|
||||
ConsistShade = 0.0;
|
||||
TDynamicObject *p = pVehicles[0]; // pojazd na czole składu
|
||||
while (p)
|
||||
@@ -4777,12 +4790,11 @@ TController::UpdateSituation(double dt) {
|
||||
|| ( vehicle->Couplers[ end::rear ].stretch_duration > 0.0 );
|
||||
// check door state
|
||||
auto const switchsides { p->DirectionGet() != iDirection };
|
||||
IsAnyDoorOpen[ side::right ] =
|
||||
IsAnyDoorOpen[ side::right ]
|
||||
|| ( false == vehicle->Doors.instances[ ( switchsides ? side::left : side::right ) ].is_closed );
|
||||
IsAnyDoorOpen[ side::left ] =
|
||||
IsAnyDoorOpen[ side::left ]
|
||||
|| ( false == vehicle->Doors.instances[ ( switchsides ? side::right : side::left ) ].is_closed );
|
||||
IsAnyDoorOpen[ side::right ] |= ( false == vehicle->Doors.instances[ ( switchsides ? side::left : side::right ) ].is_closed );
|
||||
IsAnyDoorOpen[ side::left ] |= ( false == vehicle->Doors.instances[ ( switchsides ? side::right : side::left ) ].is_closed );
|
||||
IsAnyDoorPermitActive[ side::right ] |= ( vehicle->Doors.permit_needed && vehicle->Doors.instances[ ( switchsides ? side::left : side::right ) ].open_permit );
|
||||
IsAnyDoorPermitActive[ side::left ] |= ( vehicle->Doors.permit_needed && vehicle->Doors.instances[ ( switchsides ? side::right : side::left ) ].open_permit );
|
||||
|
||||
// measure lighting level
|
||||
// TBD: apply weight (multiplier) to partially lit vehicles?
|
||||
ConsistShade += ( p->fShade > 0.0 ? p->fShade : 1.0 );
|
||||
@@ -6012,14 +6024,13 @@ TController::UpdateSituation(double dt) {
|
||||
fActionTime = -5.0; // niech trochę potrzyma
|
||||
}
|
||||
else {
|
||||
// if (iGuardRadio==iRadioChannel) //zgodność kanału
|
||||
// if (!FreeFlyModeFlag) //obserwator musi być w środku pojazdu
|
||||
// (albo może mieć radio przenośne) - kierownik mógłby powtarzać przy braku reakcji
|
||||
// TODO: proper system for sending/receiving radio messages
|
||||
// place the sound in appropriate cab of the manned vehicle
|
||||
// radio message
|
||||
tsGuardSignal.owner( pVehicle );
|
||||
/*
|
||||
tsGuardSignal.offset( { 0.f, 2.f, pVehicle->MoverParameters->Dim.L * 0.4f * ( pVehicle->MoverParameters->CabOccupied < 0 ? -1 : 1 ) } );
|
||||
tsGuardSignal.play( sound_flags::exclusive );
|
||||
*/
|
||||
simulation::radio_message( &tsGuardSignal, iGuardRadio );
|
||||
// NOTE: we can't rely on is_playing() check as sound playback is based on distance from local camera
|
||||
fActionTime = -5.0; // niech trochę potrzyma
|
||||
}
|
||||
|
||||
2
Driver.h
2
Driver.h
@@ -465,6 +465,7 @@ private:
|
||||
void Doors( bool const Open, int const Side = 0 );
|
||||
// returns true if any vehicle in the consist has an open door
|
||||
bool doors_open() const;
|
||||
bool doors_permit_active() const;
|
||||
void AutoRewident(); // ustawia hamulce w składzie
|
||||
void UpdatePantographs();
|
||||
// members
|
||||
@@ -482,6 +483,7 @@ private:
|
||||
double ConsistShade{ 1.0 }; // averaged amount of sunlight received by the consist
|
||||
TDynamicObject *pVehicles[ 2 ]; // skrajne pojazdy w składzie (niekoniecznie bezpośrednio sterowane)
|
||||
bool IsAnyDoorOpen[ 2 ]; // state of door in the consist
|
||||
bool IsAnyDoorPermitActive[ 2 ]; // state of door permit in the consist
|
||||
bool IsAnyLineBreakerOpen{ false }; // state of line breaker in all powered vehicles under control
|
||||
bool IsAnyConverterOverloadRelayOpen{ false }; // state of converter overload relays in all vehicles under control
|
||||
bool IsAnyMotorOverloadRelayOpen{ false }; // state of motor overload relays in all vehicles under control
|
||||
|
||||
17
DynObj.cpp
17
DynObj.cpp
@@ -4096,6 +4096,15 @@ void TDynamicObject::RenderSounds() {
|
||||
if( MoverParameters->CompressorSpeed > 0.0 ) {
|
||||
// McZapkie! - dzwiek compressor.wav tylko gdy dziala sprezarka
|
||||
if( MoverParameters->CompressorFlag ) {
|
||||
if( MoverParameters->CompressorPower == 3 ) {
|
||||
// presume the compressor sound is recorded for idle revolutions
|
||||
// increase the pitch according to increase of engine revolutions
|
||||
auto const enginefactor { ( MoverParameters->EngineMaxRPM() / MoverParameters->EngineIdleRPM() ) * MoverParameters->EngineRPMRatio() };
|
||||
sCompressor.pitch(
|
||||
clamp( // try to keep the sound pitch in semi-reasonable range
|
||||
enginefactor,
|
||||
0.5, 2.5 ) );
|
||||
}
|
||||
sCompressor.play( sound_flags::exclusive | sound_flags::looping );
|
||||
}
|
||||
else {
|
||||
@@ -7269,7 +7278,7 @@ TDynamicObject::powertrain_sounds::render( TMoverParameters const &Vehicle, doub
|
||||
engine.m_amplitudeoffset
|
||||
+ engine.m_amplitudefactor * (
|
||||
0.25 * ( Vehicle.EnginePower / Vehicle.Power )
|
||||
+ 0.75 * ( Vehicle.enrot * 60 ) / ( Vehicle.DElist[ Vehicle.MainCtrlPosNo ].RPM ) );
|
||||
+ 0.75 * Vehicle.EngineRPMRatio() );
|
||||
break;
|
||||
}
|
||||
case TEngineType::DieselEngine: {
|
||||
@@ -7299,11 +7308,7 @@ TDynamicObject::powertrain_sounds::render( TMoverParameters const &Vehicle, doub
|
||||
// calculate potential recent increase of engine revolutions
|
||||
auto const revolutionsperminute { Vehicle.enrot * 60 };
|
||||
auto const revolutionsdifference { revolutionsperminute - engine_revs_last };
|
||||
auto const idlerevolutionsthreshold { 1.01 * (
|
||||
Vehicle.EngineType == TEngineType::DieselElectric ?
|
||||
Vehicle.DElist[ 0 ].RPM :
|
||||
Vehicle.dizel_nmin * 60 ) };
|
||||
|
||||
auto const idlerevolutionsthreshold { 1.01 * Vehicle.EngineIdleRPM() };
|
||||
engine_revs_change = std::max( 0.0, engine_revs_change - 2.5 * Deltatime );
|
||||
if( ( revolutionsperminute > idlerevolutionsthreshold )
|
||||
&& ( revolutionsdifference > 1.0 * Deltatime ) ) {
|
||||
|
||||
245
Globals.cpp
245
Globals.cpp
@@ -656,10 +656,11 @@ global_settings::ConfigParse(cParser &Parser) {
|
||||
Parser >> token;
|
||||
auto const priority { token };
|
||||
PythonScreenUpdateRate = (
|
||||
priority == "normal" ? 200 :
|
||||
priority == "lower" ? 500 :
|
||||
priority == "lowest" ? 1000 :
|
||||
priority == "off" ? 0 :
|
||||
200 );
|
||||
stol_def( priority, 200 ) );
|
||||
}
|
||||
else if( token == "python.updatetime" )
|
||||
{
|
||||
@@ -955,3 +956,245 @@ global_settings::ConfigParse(cParser &Parser) {
|
||||
Console::ModeSet(iFeedbackMode, iFeedbackPort); // tryb pracy konsoli sterowniczej
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
global_settings::export_as_text( std::ostream &Output ) const {
|
||||
|
||||
export_as_text( Output, "sceneryfile", SceneryFile );
|
||||
export_as_text( Output, "humanctrlvehicle", local_start_vehicle );
|
||||
export_as_text( Output, "fieldofview", FieldOfView );
|
||||
export_as_text( Output, "width", iWindowWidth );
|
||||
export_as_text( Output, "height", iWindowHeight );
|
||||
export_as_text( Output, "heightbase", fDistanceFactor );
|
||||
export_as_text( Output, "targetfps", targetfps );
|
||||
export_as_text( Output, "basedrawrange", BaseDrawRange );
|
||||
export_as_text( Output, "fullscreen", bFullScreen );
|
||||
export_as_text( Output, "fullscreenmonitor", fullscreen_monitor );
|
||||
export_as_text( Output, "vsync", VSync );
|
||||
// NOTE: values are changed dynamically during simulation. cache initial settings and export instead
|
||||
if( FreeFlyModeFlag ) {
|
||||
Output
|
||||
<< "freefly yes "
|
||||
<< FreeCameraInit[ 0 ].x << " "
|
||||
<< FreeCameraInit[ 0 ].y << " "
|
||||
<< FreeCameraInit[ 0 ].z << "\n";
|
||||
}
|
||||
else {
|
||||
export_as_text( Output, "freefly", FreeFlyModeFlag );
|
||||
}
|
||||
export_as_text( Output, "wireframe", bWireFrame );
|
||||
export_as_text( Output, "debugmode", DebugModeFlag );
|
||||
export_as_text( Output, "soundenabled", bSoundEnabled );
|
||||
export_as_text( Output, "sound.openal.renderer", AudioRenderer );
|
||||
export_as_text( Output, "sound.volume", AudioVolume );
|
||||
export_as_text( Output, "sound.volume.radio", RadioVolume );
|
||||
export_as_text( Output, "physicslog", WriteLogFlag );
|
||||
export_as_text( Output, "fullphysics", FullPhysics );
|
||||
export_as_text( Output, "debuglog", iWriteLogEnabled );
|
||||
export_as_text( Output, "multiplelogs", MultipleLogs );
|
||||
export_as_text( Output, "logs.filter", DisabledLogTypes );
|
||||
Output
|
||||
<< "mousescale "
|
||||
<< fMouseXScale << " "
|
||||
<< fMouseYScale << "\n";
|
||||
export_as_text( Output, "mousecontrol", InputMouse );
|
||||
export_as_text( Output, "enabletraction", bEnableTraction );
|
||||
export_as_text( Output, "loadtraction", bLoadTraction );
|
||||
export_as_text( Output, "friction", fFriction );
|
||||
export_as_text( Output, "livetraction", bLiveTraction );
|
||||
export_as_text( Output, "skyenabled", asSky );
|
||||
export_as_text( Output, "defaultext", szDefaultExt );
|
||||
export_as_text( Output, "newaircouplers", bnewAirCouplers );
|
||||
export_as_text( Output, "anisotropicfiltering", AnisotropicFiltering );
|
||||
export_as_text( Output, "usevbo", bUseVBO );
|
||||
export_as_text( Output, "feedbackmode", iFeedbackMode );
|
||||
export_as_text( Output, "feedbackport", iFeedbackPort );
|
||||
export_as_text( Output, "multiplayer", iMultiplayer );
|
||||
export_as_text( Output, "maxtexturesize", iMaxTextureSize );
|
||||
export_as_text( Output, "maxcabtexturesize", iMaxCabTextureSize );
|
||||
export_as_text( Output, "movelight", fMoveLight );
|
||||
export_as_text( Output, "dynamiclights", DynamicLightCount );
|
||||
if( std::isnormal( ScenarioTimeOverride ) ) {
|
||||
export_as_text( Output, "scenario.time.override", ScenarioTimeOverride );
|
||||
}
|
||||
export_as_text( Output, "scenario.time.offset", ScenarioTimeOffset );
|
||||
export_as_text( Output, "scenario.time.current", ScenarioTimeCurrent );
|
||||
export_as_text( Output, "scenario.weather.temperature", AirTemperature );
|
||||
export_as_text( Output, "scalespeculars", ScaleSpecularValues );
|
||||
export_as_text( Output, "gfxrenderer", GfxRenderer );
|
||||
export_as_text( Output, "shadows", RenderShadows );
|
||||
Output
|
||||
<< "shadowtune "
|
||||
<< shadowtune.map_size << " "
|
||||
<< 0 << " "
|
||||
<< shadowtune.range << " "
|
||||
<< 0 << "\n";
|
||||
export_as_text( Output, "gfx.shadows.cab.range", RenderCabShadowsRange );
|
||||
export_as_text( Output, "gfx.smoke", Smoke );
|
||||
export_as_text( Output, "gfx.smoke.fidelity", SmokeFidelity );
|
||||
export_as_text( Output, "smoothtraction", bSmoothTraction );
|
||||
export_as_text( Output, "splinefidelity", SplineFidelity );
|
||||
export_as_text( Output, "rendercab", render_cab );
|
||||
export_as_text( Output, "createswitchtrackbeds", CreateSwitchTrackbeds );
|
||||
export_as_text( Output, "gfx.resource.sweep", ResourceSweep );
|
||||
export_as_text( Output, "gfx.resource.move", ResourceMove );
|
||||
export_as_text( Output, "gfx.reflections.framerate", 1.0 / ReflectionUpdateInterval );
|
||||
export_as_text( Output, "timespeed", fTimeSpeed );
|
||||
export_as_text( Output, "multisampling", iMultisampling );
|
||||
export_as_text( Output, "latitude", fLatitudeDeg );
|
||||
export_as_text( Output, "convertmodels", iConvertModels + ( iConvertModels > 0 ? 128 : 0 ) );
|
||||
export_as_text( Output, "file.binary.terrain", file_binary_terrain );
|
||||
export_as_text( Output, "inactivepause", bInactivePause );
|
||||
export_as_text( Output, "slowmotion", iSlowMotionMask );
|
||||
export_as_text( Output, "hideconsole", bHideConsole );
|
||||
export_as_text( Output, "rollfix", bRollFix );
|
||||
export_as_text( Output, "fpsaverage", fFpsAverage );
|
||||
export_as_text( Output, "fpsdeviation", fFpsDeviation );
|
||||
for( auto idx = 0; idx < 6; ++idx ) {
|
||||
Output
|
||||
<< "calibrate5din "
|
||||
<< idx << " "
|
||||
<< fCalibrateIn[ idx ][ 0 ] << " "
|
||||
<< fCalibrateIn[ idx ][ 1 ] << " "
|
||||
<< fCalibrateIn[ idx ][ 2 ] << " "
|
||||
<< fCalibrateIn[ idx ][ 3 ] << " "
|
||||
<< fCalibrateIn[ idx ][ 4 ] << " "
|
||||
<< fCalibrateIn[ idx ][ 5 ] << "\n";
|
||||
}
|
||||
for( auto idx = 0; idx < 6; ++idx ) {
|
||||
Output
|
||||
<< "calibrate5dout "
|
||||
<< idx << " "
|
||||
<< fCalibrateOut[ idx ][ 0 ] << " "
|
||||
<< fCalibrateOut[ idx ][ 1 ] << " "
|
||||
<< fCalibrateOut[ idx ][ 2 ] << " "
|
||||
<< fCalibrateOut[ idx ][ 3 ] << " "
|
||||
<< fCalibrateOut[ idx ][ 4 ] << " "
|
||||
<< fCalibrateOut[ idx ][ 5 ] << "\n";
|
||||
}
|
||||
Output
|
||||
<< "calibrateoutmaxvalues "
|
||||
<< fCalibrateOutMax[ 0 ] << " "
|
||||
<< fCalibrateOutMax[ 1 ] << " "
|
||||
<< fCalibrateOutMax[ 2 ] << " "
|
||||
<< fCalibrateOutMax[ 3 ] << " "
|
||||
<< fCalibrateOutMax[ 4 ] << " "
|
||||
<< fCalibrateOutMax[ 5 ] << " "
|
||||
<< fCalibrateOutMax[ 6 ] << "\n";
|
||||
export_as_text( Output, "calibrateoutdebuginfo", iCalibrateOutDebugInfo );
|
||||
for( auto idx = 0; idx < 7; ++idx ) {
|
||||
Output
|
||||
<< "pwm "
|
||||
<< idx << " "
|
||||
<< iPoKeysPWM[ idx ] << "\n";
|
||||
}
|
||||
export_as_text( Output, "brakestep", fBrakeStep );
|
||||
export_as_text( Output, "joinduplicatedevents", bJoinEvents );
|
||||
export_as_text( Output, "hiddenevents", iHiddenEvents );
|
||||
export_as_text( Output, "pause", ( iPause & 1 ) != 0 );
|
||||
export_as_text( Output, "lang", asLang );
|
||||
export_as_text( Output, "python.updatetime", PythonScreenUpdateRate );
|
||||
Output
|
||||
<< "uitextcolor "
|
||||
<< UITextColor.r * 255 << " "
|
||||
<< UITextColor.g * 255 << " "
|
||||
<< UITextColor.b * 255 << "\n";
|
||||
export_as_text( Output, "ui.bg.opacity", UIBgOpacity );
|
||||
export_as_text( Output, "input.gamepad", InputGamepad );
|
||||
#ifdef WITH_UART
|
||||
if( uart_conf.enable ) {
|
||||
Output
|
||||
<< "uart "
|
||||
<< uart_conf.port << " "
|
||||
<< uart_conf.baud << " "
|
||||
<< uart_conf.updatetime << "\n";
|
||||
}
|
||||
Output
|
||||
<< "uarttune "
|
||||
<< uart_conf.mainbrakemin << " "
|
||||
<< uart_conf.mainbrakemax << " "
|
||||
<< uart_conf.localbrakemin << " "
|
||||
<< uart_conf.localbrakemax << " "
|
||||
<< uart_conf.tankmax << " "
|
||||
<< uart_conf.tankuart << " "
|
||||
<< uart_conf.pipemax << " "
|
||||
<< uart_conf.pipeuart << " "
|
||||
<< uart_conf.brakemax << " "
|
||||
<< uart_conf.brakeuart << " "
|
||||
<< uart_conf.hvmax << " "
|
||||
<< uart_conf.hvuart << " "
|
||||
<< uart_conf.currentmax << " "
|
||||
<< uart_conf.currentuart << " "
|
||||
<< uart_conf.lvmax << " "
|
||||
<< uart_conf.lvuart << "\n";
|
||||
export_as_text( Output, "uarttachoscale", uart_conf.tachoscale );
|
||||
Output
|
||||
<< "uartfeature "
|
||||
<< uart_conf.mainenable << " "
|
||||
<< uart_conf.scndenable << " "
|
||||
<< uart_conf.trainenable << " "
|
||||
<< uart_conf.localenable << "\n";
|
||||
export_as_text( Output, "uartdebug", uart_conf.debug );
|
||||
#endif
|
||||
#ifdef USE_EXTCAM_CAMERA
|
||||
export_as_text( Output, "extcam.cmd", extcam_cmd );
|
||||
export_as_text( Output, "extcam.rec", extcam_rec );
|
||||
Output
|
||||
<< "extcam.res "
|
||||
<< extcam_res.x << " "
|
||||
<< extcam_res.y << "\n";
|
||||
#endif
|
||||
export_as_text( Output, "compresstex", compress_tex );
|
||||
export_as_text( Output, "gfx.framebuffer.width", gfx_framebuffer_width );
|
||||
export_as_text( Output, "gfx.framebuffer.height", gfx_framebuffer_height );
|
||||
export_as_text( Output, "gfx.framebuffer.fidelity", gfx_framebuffer_fidelity );
|
||||
export_as_text( Output, "gfx.shadowmap.enabled", gfx_shadowmap_enabled );
|
||||
// TODO: export fpslimit
|
||||
export_as_text( Output, "randomseed", Global.random_seed );
|
||||
export_as_text( Output, "gfx.envmap.enabled", gfx_envmap_enabled );
|
||||
export_as_text( Output, "gfx.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled );
|
||||
export_as_text( Output, "gfx.postfx.motionblur.shutter", gfx_postfx_motionblur_shutter );
|
||||
// TODO: export gfx_postfx_motionblur_format
|
||||
export_as_text( Output, "gfx.postfx.chromaticaberration.enabled", gfx_postfx_chromaticaberration_enabled );
|
||||
// TODO: export gfx_format_color
|
||||
// TODO: export gfx_format_depth
|
||||
export_as_text( Output, "gfx.skiprendering", gfx_skiprendering );
|
||||
export_as_text( Output, "gfx.skippipeline", gfx_skippipeline );
|
||||
export_as_text( Output, "gfx.extraeffects", gfx_extraeffects );
|
||||
export_as_text( Output, "python.enabled", python_enabled );
|
||||
export_as_text( Output, "python.threadedupload", python_threadedupload );
|
||||
export_as_text( Output, "python.uploadmain", python_uploadmain );
|
||||
export_as_text( Output, "python.mipmaps", python_mipmaps );
|
||||
for( auto const &server : network_servers ) {
|
||||
Output
|
||||
<< "network.server "
|
||||
<< server.first << " " << server.second << "\n";
|
||||
}
|
||||
if( false == network_client->first.empty() ) {
|
||||
Output
|
||||
<< "network.client "
|
||||
<< network_client->first << " " << network_client->second << "\n";
|
||||
}
|
||||
export_as_text( Output, "execonexit", exec_on_exit );
|
||||
}
|
||||
|
||||
template <>
|
||||
void
|
||||
global_settings::export_as_text( std::ostream &Output, std::string const Key, std::string const &Value ) const {
|
||||
|
||||
if( Value.empty() ) { return; }
|
||||
|
||||
if( Value.find( ' ' ) != std::string::npos ) {
|
||||
Output << Key << " \"" << Value << "\"\n";
|
||||
}
|
||||
else {
|
||||
Output << Key << " " << Value << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
void
|
||||
global_settings::export_as_text( std::ostream &Output, std::string const Key, bool const &Value ) const {
|
||||
|
||||
Output << Key << " " << ( Value ? "yes" : "no" ) << "\n";
|
||||
}
|
||||
|
||||
21
Globals.h
21
Globals.h
@@ -227,8 +227,29 @@ struct global_settings {
|
||||
// methods
|
||||
void LoadIniFile( std::string asFileName );
|
||||
void ConfigParse( cParser &parser );
|
||||
// sends basic content of the class in legacy (text) format to provided stream
|
||||
void
|
||||
export_as_text( std::ostream &Output ) const;
|
||||
template <typename Type_>
|
||||
void
|
||||
export_as_text( std::ostream &Output, std::string const Key, Type_ const &Value ) const;
|
||||
};
|
||||
|
||||
template <typename Type_>
|
||||
void
|
||||
global_settings::export_as_text( std::ostream &Output, std::string const Key, Type_ const &Value ) const {
|
||||
|
||||
Output << Key << " " << Value << "\n";
|
||||
}
|
||||
|
||||
template <>
|
||||
void
|
||||
global_settings::export_as_text( std::ostream &Output, std::string const Key, std::string const &Value ) const;
|
||||
|
||||
template <>
|
||||
void
|
||||
global_settings::export_as_text( std::ostream &Output, std::string const Key, bool const &Value ) const;
|
||||
|
||||
extern global_settings Global;
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
@@ -1647,6 +1647,9 @@ public:
|
||||
double PipeRatio(void);/*ile napelniac*/
|
||||
double RealPipeRatio(void);/*jak szybko*/
|
||||
double BrakeVP(void) const;
|
||||
double EngineRPMRatio() const; // returns current engine revolutions as percentage of max engine revolutions, in range 0-1
|
||||
double EngineIdleRPM() const;
|
||||
double EngineMaxRPM() const;
|
||||
|
||||
/*! przesylanie komend sterujacych*/
|
||||
bool SendCtrlToNext(std::string const CtrlCommand, double const ctrlvalue, double const dir, int const Couplertype = coupling::control );
|
||||
|
||||
@@ -1000,6 +1000,34 @@ double TMoverParameters::PipeRatio(void)
|
||||
return pr;
|
||||
}
|
||||
|
||||
double
|
||||
TMoverParameters::EngineRPMRatio() const {
|
||||
|
||||
return clamp( (
|
||||
EngineType == TEngineType::DieselElectric ? ( ( 60.0 * std::abs( enrot ) ) / DElist[ MainCtrlPosNo ].RPM ) :
|
||||
EngineType == TEngineType::DieselEngine ? ( std::abs( enrot ) / nmax ) :
|
||||
1.0 ), // shouldn't ever get here but, eh
|
||||
0.0, 1.0 );
|
||||
}
|
||||
|
||||
double
|
||||
TMoverParameters::EngineIdleRPM() const {
|
||||
|
||||
return (
|
||||
EngineType == TEngineType::DieselEngine ? dizel_nmin * 60 :
|
||||
EngineType == TEngineType::DieselElectric ? DElist[ MainCtrlNoPowerPos() ].RPM :
|
||||
std::numeric_limits<double>::max() ); // shouldn't ever get here but, eh
|
||||
}
|
||||
|
||||
double
|
||||
TMoverParameters::EngineMaxRPM() const {
|
||||
|
||||
return (
|
||||
EngineType == TEngineType::DieselEngine ? dizel_nmax * 60 :
|
||||
EngineType == TEngineType::DieselElectric ? DElist[ MainCtrlPosNo ].RPM :
|
||||
std::numeric_limits<double>::max() ); // shouldn't ever get here but, eh
|
||||
}
|
||||
|
||||
// *************************************************************************************************
|
||||
// Q: 20160716
|
||||
// Wykrywanie kolizji
|
||||
@@ -1959,17 +1987,13 @@ void TMoverParameters::OilPumpCheck( double const Timestep ) {
|
||||
OilPump.start_type == start_t::manualwithautofallback ? ( OilPump.is_enabled || dizel_startup ) :
|
||||
false ) ) ); // shouldn't ever get this far but, eh
|
||||
|
||||
auto const maxrevolutions {
|
||||
EngineType == TEngineType::DieselEngine ?
|
||||
dizel_nmax :
|
||||
DElist[ MainCtrlPosNo ].RPM / 60.0 };
|
||||
auto const minpressure {
|
||||
OilPump.pressure_minimum > 0.f ?
|
||||
OilPump.pressure_minimum :
|
||||
0.15f }; // arbitrary fallback value
|
||||
|
||||
OilPump.pressure_target = (
|
||||
enrot > 0.1 ? interpolate( minpressure, OilPump.pressure_maximum, static_cast<float>( clamp( enrot / maxrevolutions, 0.0, 1.0 ) ) ) * OilPump.resource_amount :
|
||||
enrot > 0.1 ? interpolate( minpressure, OilPump.pressure_maximum, static_cast<float>( EngineRPMRatio() ) ) * OilPump.resource_amount :
|
||||
true == OilPump.is_active ? std::min( minpressure + 0.1f, OilPump.pressure_maximum ) : // slight pressure margin to give time to switch off the pump and start the engine
|
||||
0.f );
|
||||
|
||||
@@ -4128,14 +4152,10 @@ void TMoverParameters::CompressorCheck(double dt) {
|
||||
switch( CompressorPower ) {
|
||||
case 3: {
|
||||
// the compressor is coupled with the diesel engine, engine revolutions affect the output
|
||||
auto const enginefactor { (
|
||||
EngineType == TEngineType::DieselElectric ? ( ( 60.0 * std::abs( enrot ) ) / DElist[ MainCtrlPosNo ].RPM ) :
|
||||
EngineType == TEngineType::DieselEngine ? ( std::abs( enrot ) / nmax ) :
|
||||
1.0 ) }; // shouldn't ever get here but, eh
|
||||
CompressedVolume +=
|
||||
CompressorSpeedF
|
||||
* ( 2.0 * MaxCompressorF - Compressor ) / MaxCompressorF
|
||||
* enginefactor
|
||||
* EngineRPMRatio()
|
||||
* dt
|
||||
* ( CompressorGovernorLock ? 0.0 : 1.0 ); // with the lock active air is vented out
|
||||
break;
|
||||
@@ -5217,7 +5237,7 @@ double TMoverParameters::TractionForce( double dt ) {
|
||||
std::max(
|
||||
tmp,
|
||||
std::min(
|
||||
DElist[ MainCtrlPosNo ].RPM,
|
||||
EngineMaxRPM(),
|
||||
EngineHeatingRPM )
|
||||
/ 60.0 );
|
||||
}
|
||||
@@ -5576,7 +5596,7 @@ double TMoverParameters::TractionForce( double dt ) {
|
||||
if (EIMCtrlType > 0) //sterowanie cyfrowe
|
||||
{
|
||||
auto eimic_positive = std::max(0.0, eimic_real);
|
||||
auto const rpmratio {60.0 * enrot / DElist[MainCtrlPosNo].RPM};
|
||||
auto const rpmratio { EngineRPMRatio() };
|
||||
tempImax = DElist[MainCtrlPosNo].Imax * eimic_positive;
|
||||
tempUmax = DElist[MainCtrlPosNo].Umax * std::min(eimic_positive, rpmratio);
|
||||
tempPmax = DElist[MainCtrlPosNo].GenPower * std::min(eimic_positive, rpmratio);
|
||||
@@ -5611,8 +5631,7 @@ double TMoverParameters::TractionForce( double dt ) {
|
||||
EngineVoltage = ( SST[ MainCtrlPos ].Umax * AnPos ) + ( SST[ MainCtrlPos ].Umin * ( 1.0 - AnPos ) );
|
||||
// NOTE: very crude way to approximate power generated at current rpm instead of instant top output
|
||||
// NOTE, TODO: doesn't take into account potentially increased revolutions if heating is on, fix it
|
||||
auto const rpmratio { 60.0 * enrot / DElist[ MainCtrlPos ].RPM };
|
||||
tmp = rpmratio * ( SST[ MainCtrlPos ].Pmax * AnPos ) + ( SST[ MainCtrlPos ].Pmin * ( 1.0 - AnPos ) );
|
||||
tmp = EngineRPMRatio() * ( SST[ MainCtrlPos ].Pmax * AnPos ) + ( SST[ MainCtrlPos ].Pmin * ( 1.0 - AnPos ) );
|
||||
Ft = tmp * 1000.0 / ( abs( tmpV ) + 1.6 );
|
||||
}
|
||||
else {
|
||||
@@ -6203,8 +6222,7 @@ double TMoverParameters::TractionForce( double dt ) {
|
||||
switch( EngineType ) {
|
||||
case TEngineType::DieselElectric: {
|
||||
// rough approximation of extra effort to overcome friction etc
|
||||
auto const rpmratio{ 60.0 * enrot / DElist[ MainCtrlPosNo ].RPM };
|
||||
EnginePower += rpmratio * 0.15 * DElist[ MainCtrlPosNo ].GenPower;
|
||||
EnginePower += EngineRPMRatio() * 0.15 * DElist[ MainCtrlPosNo ].GenPower;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -7780,11 +7798,7 @@ void TMoverParameters::dizel_Heat( double const dt ) {
|
||||
auto const engineoff { ( Mains ? 0 : 1 ) };
|
||||
auto const rpm { enrot * 60 };
|
||||
// TODO: calculate this once and cache for further use, instead of doing it repeatedly all over the place
|
||||
auto const maxrevolutions { (
|
||||
EngineType == TEngineType::DieselEngine ? dizel_nmax * 60 :
|
||||
EngineType == TEngineType::DieselElectric ? DElist[ MainCtrlPosNo ].RPM :
|
||||
std::numeric_limits<double>::max() ) }; // shouldn't ever get here but, eh
|
||||
auto const revolutionsfactor { clamp( rpm / maxrevolutions, 0.0, 1.0 ) };
|
||||
auto const revolutionsfactor { EngineRPMRatio() };
|
||||
auto const waterpump { WaterPump.is_active ? 1 : 0 };
|
||||
|
||||
auto const gw = engineon * interpolate( gwmin, gwmax, revolutionsfactor ) + waterpump * 1000 + engineoff * 200;
|
||||
|
||||
@@ -127,6 +127,15 @@ eu07_application::init( int Argc, char *Argv[] ) {
|
||||
WriteLog( "Authors: Marcin_EU, McZapkie, ABu, Winger, Tolaris, nbmx, OLO_EU, Bart, Quark-t, "
|
||||
"ShaXbee, Oli_EU, youBy, KURS90, Ra, hunter, szociu, Stele, Q, firleju and others\n" );
|
||||
|
||||
{
|
||||
WriteLog( "// settings" );
|
||||
std::stringstream settingspipe;
|
||||
Global.export_as_text( settingspipe );
|
||||
WriteLog( settingspipe.str() );
|
||||
}
|
||||
|
||||
WriteLog( "// startup" );
|
||||
|
||||
if( ( result = init_glfw() ) != 0 ) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -659,7 +659,6 @@ debug_panel::update_section_vehicle( std::vector<text_line> &Output ) {
|
||||
auto const &mover { *m_input.mover };
|
||||
|
||||
auto const isowned { /* ( vehicle.Mechanik == nullptr ) && */ ( vehicle.ctOwner != nullptr ) && ( vehicle.ctOwner->Vehicle() != m_input.vehicle ) };
|
||||
auto const isplayervehicle { ( m_input.train != nullptr ) && ( m_input.train->Dynamic() == m_input.vehicle ) };
|
||||
auto const isdieselenginepowered { ( mover.EngineType == TEngineType::DieselElectric ) || ( mover.EngineType == TEngineType::DieselEngine ) };
|
||||
auto const isdieselinshuntmode { mover.ShuntMode && mover.EngineType == TEngineType::DieselElectric };
|
||||
|
||||
@@ -699,7 +698,7 @@ debug_panel::update_section_vehicle( std::vector<text_line> &Output ) {
|
||||
( mover.CompressorGovernorLock ? '!' : '.' ),
|
||||
( mover.StLinSwitchOff ? '-' : ( mover.ControlPressureSwitch ? '!' : ( mover.StLinFlag ? '+' : '.' ) ) ),
|
||||
( mover.Heating ? 'H' : ( mover.HeatingAllow ? 'h' : '.' ) ),
|
||||
std::string( isplayervehicle ? locale::strings[ locale::string::debug_vehicle_radio ] + ( mover.Radio ? std::to_string( m_input.train->RadioChannel() ) : "-" ) : "" ).c_str(),
|
||||
std::string( m_input.mechanik ? locale::strings[ locale::string::debug_vehicle_radio ] + ( mover.Radio ? std::to_string( m_input.mechanik->iRadioChannel ) : "-" ) : "" ).c_str(),
|
||||
std::string( isdieselenginepowered ? locale::strings[ locale::string::debug_vehicle_oilpressure ] + to_string( mover.OilPump.pressure, 2 ) : "" ).c_str(),
|
||||
// power transfers
|
||||
// 3000v
|
||||
|
||||
22
mtable.cpp
22
mtable.cpp
@@ -57,6 +57,13 @@ bool TTrainParameters::IsMaintenance() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
int TTrainParameters::radio_channel() const {
|
||||
if( ( StationIndex < StationCount ) )
|
||||
return TimeTable[ StationIndex ].radio_channel;
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool TTrainParameters::UpdateMTable( scenario_time const &Time, std::string const &NewName ) {
|
||||
|
||||
return UpdateMTable( Time.data().wHour, Time.data().wMinute + Time.data().wSecond * 0.0167, NewName );
|
||||
@@ -406,9 +413,22 @@ bool TTrainParameters::LoadTTfile(std::string scnpath, int iPlus, double vmax)
|
||||
|| ( s == "2" )
|
||||
|| fin.bad() ) ) {
|
||||
record->StationWare += s;
|
||||
record->is_maintenance = ( s.find( "pt" ) != std::string::npos );
|
||||
fin >> s;
|
||||
}
|
||||
// cache relevant station data
|
||||
record->is_maintenance = ( s.find( "pt" ) != std::string::npos );
|
||||
{
|
||||
auto const stationware { Split( record->StationWare, ',' ) };
|
||||
for( auto const &entry : stationware ) {
|
||||
if( entry[ 0 ] == 'R' ) {
|
||||
auto const entrysplit { split_index( entry ) };
|
||||
if( ( entrysplit.first == "R" )
|
||||
&& ( entrysplit.second <= 10 ) ) {
|
||||
record->radio_channel = entrysplit.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
record->TrackNo = atoi(s.c_str());
|
||||
fin >> s;
|
||||
if (s != "|")
|
||||
|
||||
3
mtable.h
3
mtable.h
@@ -37,6 +37,7 @@ struct TMTableLine
|
||||
float Dm{ -1.f }; // godz. i min. odjazdu
|
||||
float tm{ 0.f }; // czas jazdy do tej stacji w min. (z kolumny)
|
||||
bool is_maintenance{ false };
|
||||
int radio_channel{ -1 };
|
||||
};
|
||||
|
||||
typedef TMTableLine TMTable[MaxTTableSize + 1];
|
||||
@@ -77,6 +78,8 @@ class TTrainParameters
|
||||
bool DirectionChange();
|
||||
void StationIndexInc();
|
||||
void serialize( dictionary_source *Output ) const;
|
||||
// returns: radio channel associated with current station, or -1
|
||||
int radio_channel() const;
|
||||
};
|
||||
|
||||
class TMTableTime
|
||||
|
||||
@@ -275,9 +275,10 @@ smoke_source::location() const {
|
||||
break;
|
||||
}
|
||||
case owner_type::node: {
|
||||
// TODO: take into account node rotation
|
||||
auto const rotation { glm::angleAxis( glm::radians( m_owner.node->Angles().y ), glm::vec3{ 0.f, 1.f, 0.f } ) };
|
||||
location = rotation * glm::vec3{ m_offset };
|
||||
auto const rotationx { glm::angleAxis( glm::radians( m_owner.node->Angles().x ), glm::vec3{ 1.f, 0.f, 0.f } ) };
|
||||
auto const rotationy { glm::angleAxis( glm::radians( m_owner.node->Angles().y ), glm::vec3{ 0.f, 1.f, 0.f } ) };
|
||||
auto const rotationz { glm::angleAxis( glm::radians( m_owner.node->Angles().z ), glm::vec3{ 0.f, 0.f, 1.f } ) };
|
||||
location = rotationy * rotationx * rotationz * glm::vec3{ m_offset };
|
||||
location += m_owner.node->location();
|
||||
break;
|
||||
}
|
||||
|
||||
1
sound.h
1
sound.h
@@ -17,6 +17,7 @@ float const EU07_SOUND_GLOBALRANGE { -1.f };
|
||||
float const EU07_SOUND_CABCONTROLSCUTOFFRANGE { 7.5f };
|
||||
float const EU07_SOUND_BRAKINGCUTOFFRANGE { 100.f };
|
||||
float const EU07_SOUND_RUNNINGNOISECUTOFFRANGE { 200.f };
|
||||
float const EU07_SOUND_HANDHELDRADIORANGE { 750.f };
|
||||
|
||||
enum class sound_type {
|
||||
single,
|
||||
|
||||
Reference in New Issue
Block a user