diff --git a/CMakeLists.txt b/CMakeLists.txt index 162d3c8..690b8de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.15) project(simapi VERSION 1.0.1 DESCRIPTION "Telemetry Mapping Library for Racing Sims") include(GNUInstallDirs) -add_library(simapi SHARED simapi/simmapper.c simapi/getpid.c simapi/mapping/acmapper.c simapi/mapping/pcars2mapper.c simapi/mapping/rf2mapper.c simapi/mapping/scs2mapper.c simapi/mapping/outgaugemapper.c simapi/mapping/dirt2mapper.c simapi/mapping/f12018mapper.c) +add_library(simapi SHARED simapi/simmapper.c simapi/getpid.c simapi/mapping/acmapper.c simapi/mapping/pcars2mapper.c simapi/mapping/rf2mapper.c simapi/mapping/scs2mapper.c simapi/mapping/outgaugemapper.c simapi/mapping/dirt2mapper.c simapi/mapping/f12018mapper.c simapi/mapping/wreckfest2mapper.c simapi/mapping/rbrmapper.c) set(SIMAPI_PUBLIC_HEADERS "simapi/simmapper.h" diff --git a/README.md b/README.md index 0dec44a..a631aff 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,39 @@ sudo cmake --install build ``` ## [Supported Sims](https://spacefreak18.github.io/simapi/supportedsims) -the closest to documentation found for each sim (not exhaustive) -### Assetto Corsa / Assetto Corsa Competizione -[acsharedmemorydocumentation](https://www.overtake.gg/attachments/acsharedmemorydocumentation-pdf.667802) -### RFactor2 -[modding resources](https://www.studio-397.com/modding-resources/) -[internals plugin](https://github.com/TheIronWolfModding/rF2SharedMemoryMapPlugin/blob/master/Include/InternalsPlugin.hpp) -[linux plugin download](https://github.com/schlegp/rF2SharedMemoryMapPlugin_Wine) -### Projects Cars 2 (Automobilista 2) -[sharedmemory.h](https://github.com/viper4gh/CREST2-AMS2/blob/master/SharedMemory.h) + +### Platinum Support (All features) +- Assetto Corsa +- Assetto Corsa Competizione +- Automobilista 2 +- RFactor 2 +- LeMans Ultimate + +### Gold Support (Most features) +- Project Cars 2 + +### Silver Support (Minimal features) +- Assetto Corsa Evo +- Assetto Corsa Rally +- American Truck Simulator +- Euro Truck Simulator 2 + +### Bronze Support (Very minimal features) +- Live For Speed +- BeamNG +- Dirt Rally 2 + +### Additional Support +- Richard Burns Rally RSF Edition +- Wreckfest 2 +- F1 2018/2022 + +See the full [compatibility matrix](https://spacefreak18.github.io/simapi/supportedsims) for details on protocols and plugins required for each sim. + +### Developer Documentation +- Assetto Corsa: [acsharedmemorydocumentation](https://www.overtake.gg/attachments/acsharedmemorydocumentation-pdf.667802) +- RFactor2: [modding resources](https://www.studio-397.com/modding-resources/) | [internals plugin](https://github.com/TheIronWolfModding/rF2SharedMemoryMapPlugin/blob/master/Include/InternalsPlugin.hpp) | [linux plugin](https://github.com/schlegp/rF2SharedMemoryMapPlugin_Wine) +- Project Cars 2 / AMS2: [sharedmemory.h](https://github.com/viper4gh/CREST2-AMS2/blob/master/SharedMemory.h) ## Contributing Feel free to make pull requests by adding mappings to simmapper.c for any simulator! diff --git a/include/acdata.h b/include/acdata.h index 3be9879..ec98c06 100644 --- a/include/acdata.h +++ b/include/acdata.h @@ -86,9 +86,9 @@ typedef int ACC_RAIN_INTENSITY; typedef struct //acsVec3 { - float x; - float y; - float z; + float x; + float y; + float z; } acsVec3; typedef struct //accVehicleInfo @@ -321,7 +321,7 @@ struct SPageFileStatic // car static info float maxTorque; float maxPower; - int maxRpm; + int maxRpm; float maxFuel; float suspensionMaxTravel[4]; float tyreRadius[4]; diff --git a/include/rbrdata.h b/include/rbrdata.h new file mode 100644 index 0000000..0889b73 --- /dev/null +++ b/include/rbrdata.h @@ -0,0 +1,150 @@ +// vim: set ts=4 : +/////////////////////////////////////////////////////////// +// rbr.telemetry.data.TelemetryData.h +// Implementation of the Class TireSegment +// Created on: 28-Dez-2019 07:49:08 +// Original author: Guenter Schlupf +/////////////////////////////////////////////////////////// +#ifndef _RBRDATA_H +#define _RBRDATA_H + +#include + +#pragma pack(push, 1) + +typedef struct +{ + float temperature_; + float wear_; +} RBR_TireSegment; + +typedef struct +{ + float pressure_; + float temperature_; + float carcassTemperature_; + float treadTemperature_; + unsigned int currentSegment_; + RBR_TireSegment segment1_; + RBR_TireSegment segment2_; + RBR_TireSegment segment3_; + RBR_TireSegment segment4_; + RBR_TireSegment segment5_; + RBR_TireSegment segment6_; + RBR_TireSegment segment7_; + RBR_TireSegment segment8_; +} RBR_Tire; + +typedef struct +{ + float layerTemperature_; + float temperature_; + float wear_; +} RBR_BrakeDisk; + +typedef struct +{ + RBR_BrakeDisk brakeDisk_; + RBR_Tire tire_; +} RBR_Wheel; + +typedef struct +{ + float damage_; + float pistonVelocity_; +} RBR_Damper; + +typedef struct +{ + float springDeflection_; + float rollbarForce_; + float springForce_; + float damperForce_; + float strutForce_; + int helperSpringIsActive_; + RBR_Damper damper_; + RBR_Wheel wheel_; +} RBR_Suspension; + +typedef struct +{ + float rpm_; + float radiatorCoolantTemperature_; + float engineCoolantTemperature_; + float engineTemperature_; +} RBR_Engine; + +typedef struct +{ + /// Forward/backward. + float surge_; + /// Left/right. + float sway_; + /// Up/down. + float heave_; + /// Rotation about longitudinal axis. + float roll_; + /// Rotation about transverse axis. + float pitch_; + /// Rotation about normal axis. + float yaw_; +} RBR_Motion; + +typedef struct +{ + int index_; + /// Speed of the car in kph or mph. + float speed_; + float positionX_; + float positionY_; + float positionZ_; + float roll_; + float pitch_; + float yaw_; + RBR_Motion velocities_; + RBR_Motion accelerations_; + RBR_Engine engine_; + /// Suspension data: LF, RF, LB, RB. + RBR_Suspension suspensionLF_; + /// Suspension data: LF, RF, LB, RB. + RBR_Suspension suspensionRF_; + /// Suspension data: LF, RF, LB, RB. + RBR_Suspension suspensionLB_; + /// Suspension data: LF, RF, LB, RB. + RBR_Suspension suspensionRB_; +} RBR_Car; + +typedef struct +{ + float steering_; + float throttle_; + float brake_; + float handbrake_; + float clutch_; + int gear_; + float footbrakePressure_; + float handbrakePressure_; +} RBR_Control; + +typedef struct +{ + int index_; + /// The position on the driveline. + float progress_; + /// The total race time. + float raceTime_; + float driveLineLocation_; + float distanceToEnd_; +} RBR_Stage; + +typedef struct +{ + unsigned int totalSteps_; + RBR_Stage stage_; + RBR_Control control_; + RBR_Car car_; +} RBR_TelemetryData; + +#pragma pack(pop) + +#endif // _RBRDATA_H diff --git a/include/wreckfest2data.h b/include/wreckfest2data.h new file mode 100644 index 0000000..d156dd3 --- /dev/null +++ b/include/wreckfest2data.h @@ -0,0 +1,350 @@ +/////////////////////////////////////////////////////////////////////////// +// Wreckfest 2 Telemetry Data Format (Pino) - C Compatible Version +/////////////////////////////////////////////////////////////////////////// +// Copyright (c) Bugbear Entertainment ltd. +// All Rights Reserved. +/////////////////////////////////////////////////////////////////////////// + +#ifndef _WRECKFEST2DATA_H +#define _WRECKFEST2DATA_H + +#include + +// Save current packing alignment and set it to 1 byte (no padding) +#pragma pack(push, 1) + +// Type aliases +typedef char WF2_S8; +typedef unsigned char WF2_U8; +typedef short WF2_S16; +typedef unsigned short WF2_U16; +typedef int WF2_S32; +typedef unsigned int WF2_U32; + +// Constants +#define WF2_PARTICIPANTS_MAX 36 +#define WF2_TRACK_ID_LENGTH_MAX 64 +#define WF2_TRACK_NAME_LENGTH_MAX 96 +#define WF2_CAR_ID_LENGTH_MAX 64 +#define WF2_CAR_NAME_LENGTH_MAX 96 +#define WF2_PLAYER_NAME_LENGTH_MAX 24 +#define WF2_DAMAGE_PARTS_MAX 56 +#define WF2_DAMAGE_BITS_PER_PART 3 +#define WF2_DAMAGE_BYTES_PER_PARTICIPANT 21 // (56 * 3 + 7) / 8 + +// Packet Types +enum WF2_PacketType +{ + WF2_PACKET_TYPE_MAIN = 0, + WF2_PACKET_TYPE_PARTICIPANTS_LEADERBOARD = 1, + WF2_PACKET_TYPE_PARTICIPANTS_TIMING = 2, + WF2_PACKET_TYPE_PARTICIPANTS_TIMING_SECTORS = 3, + WF2_PACKET_TYPE_PARTICIPANTS_MOTION = 4, + WF2_PACKET_TYPE_PARTICIPANTS_INFO = 5, + WF2_PACKET_TYPE_PARTICIPANTS_DAMAGE = 6 +}; + +// Game Status Flags +enum WF2_GameStatusFlags +{ + WF2_GAME_STATUS_PAUSED = (1 << 0), + WF2_GAME_STATUS_REPLAY = (1 << 1), + WF2_GAME_STATUS_SPECTATE = (1 << 2), + WF2_GAME_STATUS_MULTIPLAYER_CLIENT = (1 << 3), + WF2_GAME_STATUS_MULTIPLAYER_SERVER = (1 << 4), + WF2_GAME_STATUS_IN_RACE = (1 << 5) +}; + +// Packet Header +typedef struct +{ + WF2_U32 signature; // 1869769584 - identifies Pino packet + WF2_U8 packetType; + WF2_U8 statusFlags; + WF2_S32 sessionTime; // Primary timestamp, millisecond + WF2_S32 raceTime; // Race time from lights out, millisecond +} WF2_PacketHeader; + +// Session Information +typedef struct +{ + char trackId[WF2_TRACK_ID_LENGTH_MAX]; + char trackName[WF2_TRACK_NAME_LENGTH_MAX]; + float trackLength; // meters + + WF2_S16 laps; + WF2_S16 eventLength; // seconds + + WF2_U8 gridSize; + WF2_U8 gridSizeRemaining; + + WF2_U8 sectorCount; + float sectorFract1; + float sectorFract2; + + WF2_U8 gameMode; + WF2_U8 damageMode; + WF2_U8 status; + + char reserved[26]; +} WF2_Session; + +// Motion: Orientation +typedef struct +{ + float positionX; // meters + float positionY; + float positionZ; + + float orientationQuaternionX; + float orientationQuaternionY; + float orientationQuaternionZ; + float orientationQuaternionW; + + WF2_U16 extentsX; // centimeters + WF2_U16 extentsY; + WF2_U16 extentsZ; +} WF2_Orientation; + +// Motion: Velocity +typedef struct +{ + float velocityLocalX; // m/s + float velocityLocalY; + float velocityLocalZ; + + float angularVelocityX; // rad/s + float angularVelocityY; + float angularVelocityZ; + + float accelerationLocalX; // m/s^2 + float accelerationLocalY; + float accelerationLocalZ; +} WF2_Velocity; + +// Car: Tire +typedef struct +{ + float rps; // rad/s + float camber; // radian + float slipRatio; + float slipAngle; // radian + float radiusUnloaded; // meter + + float loadVertical; // newton + float forceLat; + float forceLong; + + float temperatureInner; // Kelvin + float temperatureTread; + + float suspensionVelocity; // m/s + float suspensionDisplacement; // meter + float suspensionDispNorm; // 0-1 + + float positionVertical; // meter + + WF2_U8 surfaceType; + + char reserved[15]; +} WF2_Tire; + +// Car: Engine +typedef struct +{ + WF2_U8 flags; + + WF2_S32 rpm; + WF2_S32 rpmMax; + WF2_S32 rpmRedline; + WF2_S32 rpmIdle; + + float torque; // newton-metre + float power; // watt + + float tempBlock; // Kelvin + float tempWater; + + float pressureManifold; // kilopascal + float pressureOil; + + char reserved[15]; +} WF2_Engine; + +// Car: Driveline +typedef struct +{ + WF2_U8 type; // FWD/RWD/AWD + + WF2_U8 gear; // 0=R, 1=N, 2=1st... + WF2_U8 gearMax; + + float speed; // m/s + + char reserved[17]; +} WF2_Driveline; + +// Car: Input +typedef struct +{ + float throttle; // 0-1 + float brake; + float clutch; + float handbrake; + float steering; // -1 to +1 +} WF2_Input; + +// Car: Assists +typedef struct +{ + WF2_U8 flags; + WF2_U8 assistGearbox; + WF2_U8 levelAbs; + WF2_U8 levelTcs; + WF2_U8 levelEsc; + + char reserved[3]; +} WF2_Assists; + +// Car: Chassis +typedef struct +{ + float trackWidth[2]; // AXLE_LOCATION_COUNT = 2 + float wheelBase; + + WF2_S32 steeringWheelLockToLock; // degrees + float steeringLock; // radian + + float cornerWeights[4]; // newton + + char reserved[16]; +} WF2_Chassis; + +// Car: Full +typedef struct +{ + WF2_Assists assists; + WF2_Chassis chassis; + WF2_Driveline driveline; + WF2_Engine engine; + WF2_Input input; + WF2_Orientation orientation; + WF2_Velocity velocity; + WF2_Tire tires[4]; // FL, FR, RL, RR + + char reserved[14]; +} WF2_CarFull; + +// Input Extended +typedef struct +{ + WF2_U8 flags; + float ffbForce; // normalized + + char reserved[15]; +} WF2_InputExtended; + +// Participant: Leaderboard +typedef struct +{ + WF2_U8 status; + WF2_U8 trackStatus; + + WF2_U16 lapCurrent; // 1st lap = 1 + WF2_U8 position; // 1st = 1 + WF2_U8 health; // 0-100% + + WF2_U16 wrecks; + WF2_U16 frags; + WF2_U16 assists; + + WF2_S32 score; + WF2_S32 points; + + WF2_S32 deltaLeader; // milliseconds + + WF2_U16 lapTiming; + + char reserved[6]; +} WF2_ParticipantLeaderboard; + +// Participant: Timing +typedef struct +{ + WF2_U32 lapTimeCurrent; // milliseconds + WF2_U32 lapTimePenaltyCurrent; + WF2_U32 lapTimeLast; + WF2_U32 lapTimeBest; + WF2_U8 lapBest; + + WF2_S32 deltaAhead; // milliseconds + WF2_S32 deltaBehind; + + float lapProgress; // 0-1 + + char reserved[3]; +} WF2_ParticipantTiming; + +// Participant: Timing Sectors +typedef struct +{ + WF2_U32 sectorTimeCurrentLap1; + WF2_U32 sectorTimeCurrentLap2; + + WF2_U32 sectorTimeLastLap1; + WF2_U32 sectorTimeLastLap2; + + WF2_U32 sectorTimeBestLap1; + WF2_U32 sectorTimeBestLap2; + + WF2_U32 sectorTimeBest1; + WF2_U32 sectorTimeBest2; + WF2_U32 sectorTimeBest3; +} WF2_ParticipantTimingSectors; + +// Participant: Info +typedef struct +{ + char carId[WF2_CAR_ID_LENGTH_MAX]; + char carName[WF2_CAR_NAME_LENGTH_MAX]; + char playerName[WF2_PLAYER_NAME_LENGTH_MAX]; + + WF2_U8 participantIndex; + + WF2_S32 lastNormalTrackStatusTime; // milliseconds + WF2_S32 lastCollisionTime; + WF2_S32 lastResetTime; + + char reserved[16]; +} WF2_ParticipantInfo; + +// Participant: Damage +typedef struct +{ + WF2_U8 damageStates[WF2_DAMAGE_BYTES_PER_PARTICIPANT]; // bit-packed +} WF2_ParticipantDamage; + +// Main Packet - Contains all player telemetry +typedef struct +{ + WF2_PacketHeader header; + + WF2_U16 marshalFlagsPlayer; + + WF2_ParticipantLeaderboard participantPlayerLeaderboard; + WF2_ParticipantTiming participantPlayerTiming; + WF2_ParticipantTimingSectors participantPlayerTimingSectors; + WF2_ParticipantInfo participantPlayerInfo; + WF2_ParticipantDamage participantPlayerDamage; + WF2_CarFull carPlayer; + WF2_Session session; + WF2_U16 playerStatusFlags; + WF2_InputExtended inputExtended; + + char reserved[106]; +} WF2_PacketMain; + +// Restore the original packing alignment +#pragma pack(pop) + +#endif // _WRECKFEST2DATA_H diff --git a/simapi/mapping/rbrmapper.c b/simapi/mapping/rbrmapper.c new file mode 100644 index 0000000..6750374 --- /dev/null +++ b/simapi/mapping/rbrmapper.c @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#include "../../include/rbrdata.h" +#include "../simdata.h" +#include "../simmap.h" +#include "../simmapper.h" + +static int froundint(float f) +{ + return (int)trunc(nearbyint(f)); +} + +/** + * @brief Maps raw Richard Burns Rally UDP telemetry to the universal SimData + * structure. + * + * Richard Burns Rally sends UDP packets containing comprehensive telemetry data. + * This function extracts those fields and maps them to the common SimData format + * used by SimAPI. + * + * Key mapping highlights: + * - Speed: Provided by RBR (likely in kph or mph, assuming kph) + * - RPM: Directly from engine data + * - Gear: RBR uses 0=Neutral, 1-6=gears, -1=Reverse (needs shifting for SimAPI) + * - Orientation: Roll, pitch, yaw are provided directly + * - Wheels: Suspension and tire data available per wheel (LF, RF, LB, RB) + * + * @param simdata The target SimData structure to populate. + * @param simmap The simulator mapping context. + * @param base Raw UDP packet pointer. + */ +void map_richard_burns_rally_data(SimData *simdata, SimMap *simmap, char* base) +{ + if (base != NULL) + { + RBR_TelemetryData *packet = (RBR_TelemetryData *)base; + + if (simmap != NULL) + { + memcpy(&simmap->rbr.rbr_telemetry, packet, sizeof(RBR_TelemetryData)); + simmap->rbr.has_telemetry = true; + } + + // Debug output - print first few values to check data integrity + static int debug_counter = 0; + if (debug_counter++ % 60 == 0) { // Print every 60 packets + fprintf(stderr, "RBR Debug: totalSteps=%u, speed=%.2f, rpm=%.2f, gear=%d, throttle=%.2f\n", + packet->totalSteps_, packet->car_.speed_, packet->car_.engine_.rpm_, + packet->control_.gear_, packet->control_.throttle_); + } + + // Basic telemetry + simdata->rpms = froundint(packet->car_.engine_.rpm_); + simdata->velocity = froundint(packet->car_.speed_); // Assuming kph + + // RBR doesn't provide max/idle RPM or max gears in telemetry, set to 0 + simdata->maxrpm = 0; + simdata->idlerpm = 0; + simdata->maxgears = 0; + + // Gear mapping: RBR uses -1=R, 0=N, 1-6=gears -> SimAPI uses 0=R, 1=N, 2-7=gears + int gear_int = packet->control_.gear_; + if (gear_int < 0) + { + simdata->gear = 0; // Reverse + } + else if (gear_int == 0) + { + simdata->gear = 1; // Neutral + } + else + { + simdata->gear = (uint32_t)gear_int + 1; // Forward gears + } + + // Character representation of gear + if (simdata->gear == 0) + { + strcpy(simdata->gearc, "R"); + } + else if (simdata->gear == 1) + { + strcpy(simdata->gearc, "N"); + } + else + { + sprintf(simdata->gearc, "%d", simdata->gear - 1); + } + + // Input mapping (already 0.0 to 1.0 presumably) + simdata->gas = (double)packet->control_.throttle_; + simdata->steer = (double)packet->control_.steering_; + simdata->brake = (double)packet->control_.brake_; + simdata->clutch = (double)packet->control_.clutch_; + + // World Velocities + simdata->worldXvelocity = (double)packet->car_.velocities_.surge_; + simdata->worldYvelocity = (double)packet->car_.velocities_.heave_; + simdata->worldZvelocity = (double)packet->car_.velocities_.sway_; + + // Orientation (RBR provides angles directly) + // Converting from radians to degrees + simdata->heading = (double)packet->car_.yaw_ * (180.0 / M_PI); + simdata->pitch = (double)packet->car_.pitch_ * (180.0 / M_PI); + simdata->roll = (double)packet->car_.roll_ * (180.0 / M_PI); + + // Accelerations (G-Forces) + // RBR provides accelerations in m/s^2, convert to G-forces (divide by 9.81) + simdata->Xvelocity = (double)packet->car_.accelerations_.surge_ / 9.81; + simdata->Yvelocity = (double)packet->car_.accelerations_.sway_ / 9.81; + simdata->Zvelocity = (double)packet->car_.accelerations_.heave_ / 9.81; + + // Position in world coordinates + simdata->worldposx = (double)packet->car_.positionX_; + simdata->worldposy = (double)packet->car_.positionY_; + simdata->worldposz = (double)packet->car_.positionZ_; + + // Suspension travel (RBR order: LF, RF, LB, RB) + // SimAPI uses: RL, RR, FL, FR (Rear Left, Rear Right, Front Left, Front Right) + // Mapping: LF->FL(2), RF->FR(3), LB->RL(0), RB->RR(1) + simdata->suspension[2] = (double)packet->car_.suspensionLF_.springDeflection_; + simdata->suspension[3] = (double)packet->car_.suspensionRF_.springDeflection_; + simdata->suspension[0] = (double)packet->car_.suspensionLB_.springDeflection_; + simdata->suspension[1] = (double)packet->car_.suspensionRB_.springDeflection_; + + // Suspension velocities + simdata->suspvelocity[2] = (double)packet->car_.suspensionLF_.damper_.pistonVelocity_; + simdata->suspvelocity[3] = (double)packet->car_.suspensionRF_.damper_.pistonVelocity_; + simdata->suspvelocity[0] = (double)packet->car_.suspensionLB_.damper_.pistonVelocity_; + simdata->suspvelocity[1] = (double)packet->car_.suspensionRB_.damper_.pistonVelocity_; + + // Tire temperatures (using tread temperature) + simdata->tyretemp[2] = (double)packet->car_.suspensionLF_.wheel_.tire_.treadTemperature_; + simdata->tyretemp[3] = (double)packet->car_.suspensionRF_.wheel_.tire_.treadTemperature_; + simdata->tyretemp[0] = (double)packet->car_.suspensionLB_.wheel_.tire_.treadTemperature_; + simdata->tyretemp[1] = (double)packet->car_.suspensionRB_.wheel_.tire_.treadTemperature_; + + // Tire pressures + simdata->tyrepressure[2] = (double)packet->car_.suspensionLF_.wheel_.tire_.pressure_; + simdata->tyrepressure[3] = (double)packet->car_.suspensionRF_.wheel_.tire_.pressure_; + simdata->tyrepressure[0] = (double)packet->car_.suspensionLB_.wheel_.tire_.pressure_; + simdata->tyrepressure[1] = (double)packet->car_.suspensionRB_.wheel_.tire_.pressure_; + + // Brake temperatures + simdata->braketemp[2] = (double)packet->car_.suspensionLF_.wheel_.brakeDisk_.temperature_; + simdata->braketemp[3] = (double)packet->car_.suspensionRF_.wheel_.brakeDisk_.temperature_; + simdata->braketemp[0] = (double)packet->car_.suspensionLB_.wheel_.brakeDisk_.temperature_; + simdata->braketemp[1] = (double)packet->car_.suspensionRB_.wheel_.brakeDisk_.temperature_; + + // Engine temperatures (using available fields) + // simdata->airtemp = (double)packet->car_.engine_.engineTemperature_; + // simdata->tracktemp = (double)packet->car_.engine_.engineCoolantTemperature_; + + // Stage/Race information + simdata->trackdistancearound = (double)packet->stage_.distanceToEnd_; + simdata->playerspline = (double)packet->stage_.progress_; + + // Timing + simdata->currentlapinseconds = (uint32_t)packet->stage_.raceTime_; + + // Split seconds into LapTime structure + simdata->currentlap.hours = (uint32_t)(packet->stage_.raceTime_ / 3600); + simdata->currentlap.minutes = + (uint32_t)(((int)packet->stage_.raceTime_ % 3600) / 60); + simdata->currentlap.seconds = (uint32_t)((int)packet->stage_.raceTime_ % 60); + simdata->currentlap.fraction = + (uint32_t)((packet->stage_.raceTime_ - floor(packet->stage_.raceTime_)) * 1000); + + // Game Status + // RBR is actively sending telemetry, so assume active play + simdata->simstatus = SIMAPI_STATUS_ACTIVEPLAY; + + strncpy(simdata->car, "RBR", 4); + } +} diff --git a/simapi/mapping/wreckfest2mapper.c b/simapi/mapping/wreckfest2mapper.c new file mode 100644 index 0000000..80b4bc1 --- /dev/null +++ b/simapi/mapping/wreckfest2mapper.c @@ -0,0 +1,238 @@ +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#include "../simmap.h" +#include "../simdata.h" +#include "../simapi.h" +#include "../simmapper.h" +#include "../wreckfest2.h" +#include "../../include/wreckfest2data.h" + +// Forward declarations for helper functions +static void map_main_packet(SimData* simdata, SimMap* simmap, char* base); +static void quaternion_to_euler(float qx, float qy, float qz, float qw, + double* heading, double* pitch, double* roll); + +/** + * @brief Maps Wreckfest 2 Pino UDP telemetry to universal SimData + * + * Wreckfest 2 uses multi-packet telemetry at different frequencies: + * - Main packet (60Hz) - Player telemetry, session data + * - Participants packets (60Hz) - Leaderboard, timing, motion + * - Damage/Info packets (2Hz/1Hz) + * + * All packets must be synchronized by sessionTime field in header. + */ +void map_wreckfest2_data(SimData* simdata, SimMap* simmap, char* base) +{ + if (base == NULL) return; + + // Identify packet type from header + WF2_PacketHeader* header = (WF2_PacketHeader*)base; + + // Verify Pino packet signature (1869769584) + if (header->signature != 1869769584) { + return; + } + + // Route based on packet type + // For now, we primarily handle the Main packet which contains all essential player data + switch (header->packetType) { + case WF2_PACKET_TYPE_MAIN: + map_main_packet(simdata, simmap, base); + break; + case WF2_PACKET_TYPE_PARTICIPANTS_LEADERBOARD: + // Future: Handle participant leaderboard data for multiplayer + break; + case WF2_PACKET_TYPE_PARTICIPANTS_TIMING: + // Future: Handle participant timing data for multiplayer + break; + default: + // Ignore other packet types for now + break; + } +} + +/** + * @brief Maps the main telemetry packet containing player data + */ +static void map_main_packet(SimData* simdata, SimMap* simmap, char* base) +{ + WF2_PacketMain* packet = (WF2_PacketMain*)base; + + // Store packet for synchronization + if (simmap != NULL) { + memcpy(&simmap->wf2.main_packet, packet, sizeof(*packet)); + simmap->wf2.has_main_packet = true; + simmap->wf2.last_session_time = packet->header.sessionTime; + } + + // Basic telemetry from Car::Full + simdata->rpms = (uint32_t)packet->carPlayer.engine.rpm; + simdata->maxrpm = (uint32_t)packet->carPlayer.engine.rpmMax; + simdata->idlerpm = (uint32_t)packet->carPlayer.engine.rpmIdle; + + // Gear mapping: Wreckfest uses 0=R, 1=N, 2=1st... which matches SimAPI + simdata->gear = (uint32_t)packet->carPlayer.driveline.gear; + simdata->maxgears = (uint32_t)packet->carPlayer.driveline.gearMax; + + // Set gear character representation + if (simdata->gear == 0) { + strcpy(simdata->gearc, "R"); + } else if (simdata->gear == 1) { + strcpy(simdata->gearc, "N"); + } else { + sprintf(simdata->gearc, "%d", simdata->gear - 1); + } + + // Velocity: m/s to km/h + simdata->velocity = (uint32_t)(packet->carPlayer.driveline.speed * 3.6f); + + // Inputs (already 0-1 range) + simdata->gas = (double)packet->carPlayer.input.throttle; + simdata->brake = (double)packet->carPlayer.input.brake; + simdata->clutch = (double)packet->carPlayer.input.clutch; + simdata->steer = (double)packet->carPlayer.input.steering; + simdata->handbrake = (double)packet->carPlayer.input.handbrake; + + // Position from Motion::Orientation + simdata->worldposx = (double)packet->carPlayer.orientation.positionX; + simdata->worldposy = (double)packet->carPlayer.orientation.positionY; + simdata->worldposz = (double)packet->carPlayer.orientation.positionZ; + + // Local velocity (m/s) + simdata->Xvelocity = (double)packet->carPlayer.velocity.velocityLocalX; + simdata->Yvelocity = (double)packet->carPlayer.velocity.velocityLocalY; + simdata->Zvelocity = (double)packet->carPlayer.velocity.velocityLocalZ; + + // Angular velocity (rad/s) - store in world velocity fields + simdata->worldXvelocity = (double)packet->carPlayer.velocity.angularVelocityX; + simdata->worldYvelocity = (double)packet->carPlayer.velocity.angularVelocityY; + simdata->worldZvelocity = (double)packet->carPlayer.velocity.angularVelocityZ; + + // Convert quaternion to Euler angles (degrees) + quaternion_to_euler( + packet->carPlayer.orientation.orientationQuaternionX, + packet->carPlayer.orientation.orientationQuaternionY, + packet->carPlayer.orientation.orientationQuaternionZ, + packet->carPlayer.orientation.orientationQuaternionW, + &simdata->heading, + &simdata->pitch, + &simdata->roll + ); + + // Tire data (4 tires: FL=0, FR=1, RL=2, RR=3 in Wreckfest) + // Note: Wreckfest uses different order than some sims + for (int i = 0; i < 4; i++) { + simdata->tyreRPS[i] = (double)packet->carPlayer.tires[i].rps; + + // Temperature - convert Kelvin to Celsius if available + float temp_tread = packet->carPlayer.tires[i].temperatureTread; + if (temp_tread > 0.0f) { + simdata->tyretemp[i] = (double)(temp_tread - 273.15); + } else { + simdata->tyretemp[i] = 0.0; + } + + // Tire pressure - not available in current Pino implementation + simdata->tyrepressure[i] = 0.0; + + // Suspension position and velocity + simdata->suspension[i] = (double)packet->carPlayer.tires[i].suspensionDisplacement; + simdata->suspvelocity[i] = (double)packet->carPlayer.tires[i].suspensionVelocity; + } + + // Session info + strncpy(simdata->track, packet->session.trackName, 127); + simdata->track[127] = '\0'; + simdata->numlaps = (uint32_t)packet->session.laps; + + // Player info from participant data + strncpy(simdata->driver, packet->participantPlayerInfo.playerName, 127); + simdata->driver[127] = '\0'; + strncpy(simdata->car, packet->participantPlayerInfo.carName, 127); + simdata->car[127] = '\0'; + + // Lap and position from leaderboard + simdata->lap = (uint32_t)packet->participantPlayerLeaderboard.lapCurrent; + simdata->position = (uint32_t)packet->participantPlayerLeaderboard.position; + simdata->playerlaps = simdata->lap > 0 ? simdata->lap - 1 : 0; + + // Timing from participant timing (convert milliseconds to seconds) + simdata->currentlapinseconds = packet->participantPlayerTiming.lapTimeCurrent / 1000; + simdata->lastlapinseconds = packet->participantPlayerTiming.lapTimeLast / 1000; + + // Convert milliseconds to LapTime struct + uint32_t current_ms = packet->participantPlayerTiming.lapTimeCurrent; + simdata->currentlap.hours = current_ms / 3600000; + simdata->currentlap.minutes = (current_ms % 3600000) / 60000; + simdata->currentlap.seconds = (current_ms % 60000) / 1000; + simdata->currentlap.fraction = current_ms % 1000; + + uint32_t last_ms = packet->participantPlayerTiming.lapTimeLast; + simdata->lastlap.hours = last_ms / 3600000; + simdata->lastlap.minutes = (last_ms % 3600000) / 60000; + simdata->lastlap.seconds = (last_ms % 60000) / 1000; + simdata->lastlap.fraction = last_ms % 1000; + + uint32_t best_ms = packet->participantPlayerTiming.lapTimeBest; + simdata->bestlap.hours = best_ms / 3600000; + simdata->bestlap.minutes = (best_ms % 3600000) / 60000; + simdata->bestlap.seconds = (best_ms % 60000) / 1000; + simdata->bestlap.fraction = best_ms % 1000; + + // Lap progress along track route (0-1) + simdata->playerspline = (double)packet->participantPlayerTiming.lapProgress; + simdata->trackdistancearound = (double)packet->session.trackLength; + + // Game status + if (packet->header.statusFlags & WF2_GAME_STATUS_IN_RACE) { + simdata->simstatus = SIMAPI_STATUS_ACTIVEPLAY; + } else { + simdata->simstatus = SIMAPI_STATUS_MENU; + } + + // Engine temps (Kelvin to Celsius) + simdata->airtemp = (double)(packet->carPlayer.engine.tempBlock - 273.15); + simdata->tracktemp = (double)(packet->carPlayer.engine.tempWater - 273.15); + + // Engine power and torque + simdata->turboboost = (double)packet->carPlayer.engine.pressureManifold; + + // Additional car data + simdata->abs = (double)packet->carPlayer.assists.levelAbs; +} + +/** + * @brief Convert quaternion to Euler angles (heading, pitch, roll) in degrees + * + * Uses ZYX rotation order (yaw-pitch-roll) + * Wreckfest 2 uses left-handed coordinate system: X=right, Y=up, Z=forward + */ +static void quaternion_to_euler(float qx, float qy, float qz, float qw, + double* heading, double* pitch, double* roll) +{ + // Heading (Yaw) - rotation around Y axis + double siny_cosp = 2.0 * (qw * qy + qx * qz); + double cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz); + *heading = atan2(siny_cosp, cosy_cosp) * (180.0 / M_PI); + + // Pitch - rotation around X axis + double sinp = 2.0 * (qw * qx - qz * qy); + if (fabs(sinp) >= 1.0) { + *pitch = copysign(90.0, sinp); // Use 90 degrees if out of range + } else { + *pitch = asin(sinp) * (180.0 / M_PI); + } + + // Roll - rotation around Z axis + double sinr_cosp = 2.0 * (qw * qz + qx * qy); + double cosr_cosp = 1.0 - 2.0 * (qx * qx + qy * qy); + *roll = atan2(sinr_cosp, cosr_cosp) * (180.0 / M_PI); +} diff --git a/simapi/rbr.h b/simapi/rbr.h new file mode 100644 index 0000000..8e66a5b --- /dev/null +++ b/simapi/rbr.h @@ -0,0 +1,16 @@ +#ifndef _RBR_H +#define _RBR_H + +#include +#include "../include/rbrdata.h" + +#define RICHARD_BURNS_RALLY_EXE "RichardBurnsRally_SSE.exe" + +typedef struct +{ + bool has_telemetry; + RBR_TelemetryData rbr_telemetry; +} +RBRMap; + +#endif diff --git a/simapi/simapi.h b/simapi/simapi.h index 27bfe2f..cd61a44 100644 --- a/simapi/simapi.h +++ b/simapi/simapi.h @@ -32,6 +32,8 @@ typedef enum SIMULATORAPI_RACE_ROOM = 8, SIMULATORAPI_FORZA = 9, SIMULATORAPI_LMU = 10, + SIMULATORAPI_WRECKFEST2 = 11, + SIMULATORAPI_RICHARD_BURNS_RALLY = 12, } SimulatorAPI; @@ -53,6 +55,8 @@ typedef enum SIMULATOREXE_F1_2022 = 1692250, //f122 SIMULATOREXE_RACE_ROOM = 211500, //r3e SIMULATOREXE_FORZA_HORIZON_5 = 1551360, //fh5 + SIMULATOREXE_WRECKFEST2 = 1203190, //wf2 + SIMULATOREXE_RICHARD_BURNS_RALLY = 0000002, //rbr } SimulatorEXE; diff --git a/simapi/simmap.h b/simapi/simmap.h index 96168c9..0bff493 100644 --- a/simapi/simmap.h +++ b/simapi/simmap.h @@ -6,6 +6,8 @@ #include "pcars2.h" #include "scs2.h" #include "dirt2.h" +#include "wreckfest2.h" +#include "rbr.h" struct _simmap { @@ -18,6 +20,8 @@ struct _simmap PCars2Map pcars2; SCS2Map scs2; Dirt2Map dirt2; + Wreckfest2Map wf2; + RBRMap rbr; }; #endif diff --git a/simapi/simmapper.c b/simapi/simmapper.c index 48325db..67d37f6 100755 --- a/simapi/simmapper.c +++ b/simapi/simmapper.c @@ -22,6 +22,8 @@ #include "outgauge.h" #include "dirt2.h" #include "f1.h" +#include "wreckfest2.h" +#include "rbr.h" #include "simmap.h" #include @@ -152,6 +154,14 @@ int simapi_strtogame(const char* game) { sim = SIMULATOREXE_RACE_ROOM; } + else if (sstrcicmp(game, "wf2") == 0) + { + sim = SIMULATOREXE_WRECKFEST2; + } + else if (sstrcicmp(game, "rbr") == 0) + { + sim = SIMULATOREXE_RICHARD_BURNS_RALLY; + } else { sim = 0; @@ -193,6 +203,10 @@ char* simapi_gametostr(SimulatorEXE sim) return "fh5"; case SIMULATOREXE_RACE_ROOM: return "r3e"; + case SIMULATOREXE_WRECKFEST2: + return "wf2"; + case SIMULATOREXE_RICHARD_BURNS_RALLY: + return "rbr"; default: return "default"; } @@ -232,6 +246,10 @@ char* simapi_gametofullstr(SimulatorEXE sim) return "Forza Horizon 5"; case SIMULATOREXE_RACE_ROOM: return "Race Room"; + case SIMULATOREXE_WRECKFEST2: + return "Wreckfest 2"; + case SIMULATOREXE_RICHARD_BURNS_RALLY: + return "Richard Burns Rally"; default: return "default"; } @@ -485,6 +503,13 @@ int setSimInfo(SimInfo* si) si->SimSupportsRealtimeTelemetry = false; si->SimSupportsAdvancedUI = false; break; + case SIMULATORAPI_RICHARD_BURNS_RALLY: + si->SimUsesUDP = true; + si->SimSupportsBasicTelemetry = true; + si->SimSupportsTyreEffects = true; + si->SimSupportsRealtimeTelemetry = true; + si->SimSupportsAdvancedUI = false; + break; default: si->SimSupportsBasicTelemetry = true; } @@ -628,6 +653,18 @@ SimulatorEXE getSimExe(SimInfo* si) si->pid = pid; return SIMULATOREXE_F1_2022; } + pid = IsProcessRunning(WRECKFEST2_EXE); + if(pid>0) + { + si->pid = pid; + return SIMULATOREXE_WRECKFEST2; + } + pid = IsProcessRunning(RICHARD_BURNS_RALLY_EXE); + if(pid>0) + { + si->pid = pid; + return SIMULATOREXE_RICHARD_BURNS_RALLY; + } //pid = IsProcessRunning(FORZA_HORIZON_5_EXE); //if(pid>0) //{ @@ -944,6 +981,66 @@ SimInfo getSim(SimData* simdata, SimMap* simmap, bool force_udp, int (*setup_udp return si; } break; + case SIMULATOREXE_WRECKFEST2: + simapi_log(SIMAPI_LOGLEVEL_DEBUG, "Found running process for Wreckfest 2"); + int wf2_error = 0; + if (*setup_udp != NULL) + { + wf2_error = (*setup_udp)(23123); + } + + if (wf2_error == 0) + { + wf2_error = siminitudp(simdata, simmap, SIMULATORAPI_WRECKFEST2); + } + + if (wf2_error == 0) + { + simdata->simon = true; + simdata->simapi = SIMULATORAPI_WRECKFEST2; + simdata->simexe = simexe; + simdata->simstatus = SIMAPI_STATUS_ACTIVEPLAY; + + si.isSimOn = true; + si.SimUsesUDP = true; + si.simulatorapi = simdata->simapi; + si.mapapi = si.simulatorapi; + si.simulatorexe = simdata->simexe; + setSimInfo(&si); + + return si; + } + break; + case SIMULATOREXE_RICHARD_BURNS_RALLY: + simapi_log(SIMAPI_LOGLEVEL_DEBUG, "Found running process for Richard Burns Rally"); + int rbr_error = 0; + if (*setup_udp != NULL) + { + rbr_error = (*setup_udp)(6776); + } + + if (rbr_error == 0) + { + rbr_error = siminitudp(simdata, simmap, SIMULATORAPI_RICHARD_BURNS_RALLY); + } + + if (rbr_error == 0) + { + simdata->simon = true; + simdata->simapi = SIMULATORAPI_RICHARD_BURNS_RALLY; + simdata->simexe = simexe; + simdata->simstatus = SIMAPI_STATUS_ACTIVEPLAY; + + si.isSimOn = true; + si.SimUsesUDP = true; + si.simulatorapi = simdata->simapi; + si.mapapi = si.simulatorapi; + si.simulatorexe = simdata->simexe; + setSimInfo(&si); + + return si; + } + break; case SIMULATOREXE_RACE_ROOM: case SIMULATOREXE_FORZA_HORIZON_5: break; @@ -1015,6 +1112,16 @@ int simdatamap(SimData* simdata, SimMap* simmap, SimMap* simmap2, SimulatorAPI s map_f1_2018_data(simdata, simmap, base); break; + case SIMULATORAPI_WRECKFEST2 : + + map_wreckfest2_data(simdata, simmap, base); + break; + + case SIMULATORAPI_RICHARD_BURNS_RALLY : + + map_richard_burns_rally_data(simdata, simmap, base); + break; + case SIMULATORAPI_FORZA: break; case SIMULATORAPI_RACE_ROOM: diff --git a/simapi/simmapper.h b/simapi/simmapper.h index 7ffcddc..cb41fce 100644 --- a/simapi/simmapper.h +++ b/simapi/simmapper.h @@ -89,5 +89,7 @@ void map_trucks_data(SimData* simdata, SimMap* simmap); void map_outgauge_outsim_data(SimData* simdata, SimMap* simmap, SimulatorEXE simexe, char* base); void map_dirt_rally_2_data(SimData* simdata, SimMap* simmap, char* base); void map_f1_2018_data(SimData* simdata, SimMap* simmap, char* base); +void map_wreckfest2_data(SimData* simdata, SimMap* simmap, char* base); +void map_richard_burns_rally_data(SimData* simdata, SimMap* simmap, char* base); #endif diff --git a/simapi/wreckfest2.h b/simapi/wreckfest2.h new file mode 100644 index 0000000..a83382b --- /dev/null +++ b/simapi/wreckfest2.h @@ -0,0 +1,22 @@ +#ifndef _WRECKFEST2_H +#define _WRECKFEST2_H + +#include +#include +#include "../include/wreckfest2data.h" + +#define WRECKFEST2_EXE "Wreckfest2.exe" + +typedef struct +{ + bool has_main_packet; + bool has_leaderboard_packet; + bool has_timing_packet; + + // Store last received packets for multi-packet sync + WF2_PacketMain main_packet; + + int32_t last_session_time; // For packet synchronization +} Wreckfest2Map; + +#endif diff --git a/simd/CMakeLists.txt b/simd/CMakeLists.txt index 0e81b72..ddce227 100644 --- a/simd/CMakeLists.txt +++ b/simd/CMakeLists.txt @@ -34,6 +34,23 @@ target_link_libraries(simd m uv yder ${ARGTABLE_LIBS} config simapi) target_include_directories(simd PRIVATE ${ARGTABLE_INCLUDE_DIR}) +# User-level installation +install(TARGETS simd RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + +# Install systemd user service file to ~/.config/systemd/user/ +option(INSTALL_SYSTEMD_SERVICE "Install systemd user service file" ON) +if(INSTALL_SYSTEMD_SERVICE) + set(SYSTEMD_USER_UNIT_DIR "$ENV{HOME}/.config/systemd/user" CACHE PATH "Systemd user unit directory") + install(FILES conf/simd.service DESTINATION ${SYSTEMD_USER_UNIT_DIR}) +endif() + +# Install default config file to ~/.config/simd/ +option(INSTALL_DEFAULT_CONFIG "Install default configuration file" ON) +if(INSTALL_DEFAULT_CONFIG) + set(SIMD_CONFIG_DIR "$ENV{HOME}/.config/simd" CACHE PATH "simd configuration directory") + install(FILES conf/simd.config DESTINATION ${SIMD_CONFIG_DIR}) +endif() + # used for enabling additional compiler options if supported include(CheckCXXCompilerFlag) diff --git a/simd/README.md b/simd/README.md index c1275b7..c1e4beb 100644 --- a/simd/README.md +++ b/simd/README.md @@ -25,6 +25,41 @@ cmake -B build cmake --build build ``` +## User-Level Installation + +To install simd as a user-level systemd service: + +```bash +# Build and install to ~/.local (includes binary, service file, and config) +cmake -B build -DCMAKE_INSTALL_PREFIX=$HOME/.local +cmake --build build +cmake --install build + +# Reload systemd user daemon and enable the service +systemctl --user daemon-reload +systemctl --user enable --now simd +``` + +This installs: +- Binary to `~/.local/bin/simd` +- Systemd service to `~/.config/systemd/user/simd.service` +- Config file to `~/.config/simd/simd.config` + +To manage the service: +```bash +systemctl --user status simd # Check status +systemctl --user stop simd # Stop the service +systemctl --user restart simd # Restart the service +journalctl --user -u simd -f # View logs +``` + +To disable installation of specific components: +```bash +cmake -B build -DCMAKE_INSTALL_PREFIX=$HOME/.local \ + -DINSTALL_SYSTEMD_SERVICE=OFF \ + -DINSTALL_DEFAULT_CONFIG=OFF +``` + ## Usage if you wish to use the automatic bridging mode, first copy the config file from [here](https://github.com/Spacefreak18/simapi/blob/master/simd/conf/simd.config) to ~/.config/simd/simd.config and add the appropriate [bridge](https://github.com/spacefreak18/simshmbridge) exe to your steam launch command like this: diff --git a/simd/conf/simd.service b/simd/conf/simd.service new file mode 100644 index 0000000..b8fcfa5 --- /dev/null +++ b/simd/conf/simd.service @@ -0,0 +1,18 @@ +[Unit] +Description=SimAPI Daemon - Racing Simulator Telemetry Service +Documentation=https://github.com/Spacefreak18/simapi +After=default.target + +[Service] +Type=simple +Environment="LD_LIBRARY_PATH=%h/.local/lib" +ExecStart=%h/.local/bin/simd --nodaemon +Restart=on-failure +RestartSec=5 + +# Logging goes to journald +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=default.target diff --git a/simd/simd.c b/simd/simd.c index 9c7a57c..5399913 100644 --- a/simd/simd.c +++ b/simd/simd.c @@ -45,12 +45,17 @@ SimCompatMap* compatmap; GameCompatInfo* game_compat_info; SimdSettings simds; +struct termios canonicalmode; +int stdin_is_tty = 0; + uv_poll_t pollt; uv_timer_t gamefindtimer; uv_timer_t datachecktimer; uv_timer_t datamaptimer; uv_timer_t bridgeclosetimer; uv_udp_t recv_socket; +bool recv_socket_initialized = false; +bool recv_socket_bound = false; int appstate = 0; int compat_info_size = 0; @@ -137,7 +142,10 @@ void release() uv_timer_stop(&gamefindtimer); uv_timer_stop(&datamaptimer); uv_timer_stop(&datachecktimer); - uv_udp_recv_stop(&recv_socket); + if (recv_socket_initialized) + { + uv_udp_recv_stop(&recv_socket); + } uv_timer_stop(&bridgeclosetimer); uv_walk(uv_default_loop(), close_walk_cb, NULL); uv_run(uv_default_loop(), UV_RUN_DEFAULT); @@ -175,10 +183,17 @@ void release() y_close_logs(); } -// Signal handler for SIGTERM +// Signal handler for SIGTERM and SIGINT void handle_sigterm(int sig) { - y_log_message(Y_LOG_LEVEL_DEBUG, "SIGTERM received. Exiting gracefully..."); + const char* signame = (sig == SIGTERM) ? "SIGTERM" : "SIGINT"; + y_log_message(Y_LOG_LEVEL_DEBUG, "%s received. Exiting gracefully...", signame); + + // Restore terminal settings if we modified them + if (stdin_is_tty) + { + tcsetattr(0, TCSANOW, &canonicalmode); + } release(); exit(0); @@ -206,9 +221,19 @@ void releaseloop(LoopData* f, SimData* simdata, SimMap* simmap) simdmap(simmap2, simdata); } // Properly close the UDP socket if it's open - if (uv_is_active((uv_handle_t*)&recv_socket)) + if (recv_socket_initialized) { - uv_udp_recv_stop(&recv_socket); + if (uv_is_active((uv_handle_t*)&recv_socket)) + { + uv_udp_recv_stop(&recv_socket); + } + // Close the socket handle so it can be reinitialized with a different port + if (!uv_is_closing((uv_handle_t*)&recv_socket)) + { + uv_close((uv_handle_t*)&recv_socket, NULL); + } + recv_socket_initialized = false; + recv_socket_bound = false; } int r = simfree(simdata, simmap, f->sim); @@ -305,15 +330,31 @@ static void on_udp_recv(uv_udp_t* handle, ssize_t nread, const uv_buf_t* rcvbuf, int startudp(int port) { - if (uv_is_active((uv_handle_t*)&recv_socket) || uv_is_closing((uv_handle_t*)&recv_socket)) + // Already bound successfully - nothing to do + if (recv_socket_bound) { return 0; } - uv_udp_init(uv_default_loop(), &recv_socket); + // Socket active or closing - wait for it to settle + if (recv_socket_initialized && (uv_is_active((uv_handle_t*)&recv_socket) || uv_is_closing((uv_handle_t*)&recv_socket))) + { + return 0; + } + if (!recv_socket_initialized) + { + uv_udp_init(uv_default_loop(), &recv_socket); + uv_handle_set_data((uv_handle_t*) &recv_socket, (void*) baton); + recv_socket_initialized = true; + } struct sockaddr_in recv_addr; uv_ip4_addr("0.0.0.0", port, &recv_addr); int err = uv_udp_bind(&recv_socket, (const struct sockaddr *) &recv_addr, UV_UDP_REUSEADDR); - y_log_message(Y_LOG_LEVEL_DEBUG, "initial udp error is %i for port %i", err, port); + y_log_message(Y_LOG_LEVEL_DEBUG, "udp bind result is %i for port %i", err, port); + + if (err == 0) + { + recv_socket_bound = true; + } return err; } @@ -582,6 +623,12 @@ void gamefindcallback(uv_timer_t* handle) y_log_message(Y_LOG_LEVEL_DEBUG, "Could not fork a bridge process"); } } + else + { + y_log_message(Y_LOG_LEVEL_WARNING, "Bridge setup failed, continuing without bridge."); + uv_timer_start(&datachecktimer, datacheckcallback, 0, 1000); + uv_timer_stop(handle); + } } else { @@ -688,7 +735,10 @@ void cb(uv_poll_t* handle, int status, int events) { y_log_message(Y_LOG_LEVEL_INFO, "simd is exiting..."); uv_timer_stop(&datachecktimer); - uv_udp_recv_stop(&recv_socket); + if (recv_socket_initialized) + { + uv_udp_recv_stop(&recv_socket); + } uv_timer_stop(&bridgeclosetimer); uv_timer_stop(&gamefindtimer); uv_poll_stop(handle); @@ -727,6 +777,12 @@ int main(int argc, char** argv) ylog_mode = Y_LOG_MODE_CONSOLE; } + int ylog_level = Y_LOG_LEVEL_INFO; + if(p->verbosity_count > 0) + { + ylog_level = Y_LOG_LEVEL_DEBUG; + } + int pid_file_fd = open(PID_FILE, O_WRONLY | O_CREAT | O_EXCL, 0666); if(pid_file_fd == -1) @@ -742,7 +798,7 @@ int main(int argc, char** argv) } close(pid_file_fd); - y_init_logs("simd", ylog_mode, Y_LOG_LEVEL_DEBUG, "/tmp/simd.log", "Initializing logs mode: file, logs level: debug"); + y_init_logs("simd", ylog_mode, ylog_level, "/tmp/simd.log", "Initializing logs"); y_log_message(Y_LOG_LEVEL_INFO, "Started. Found home directory and interpreted parameters.\n"); // config file @@ -778,7 +834,12 @@ int main(int argc, char** argv) y_log_message(Y_LOG_LEVEL_INFO, "Successfullly loaded configuration file"); } - struct termios newsettings, canonicalmode; + struct termios newsettings; + + /* Register signal handlers for clean shutdown (both daemon and non-daemon modes) */ + signal(SIGTERM, handle_sigterm); + signal(SIGINT, handle_sigterm); + if(simds.daemon == true) { pid_t pid; @@ -808,7 +869,6 @@ int main(int argc, char** argv) //TODO: Implement a working signal handler */ signal(SIGCHLD, SIG_IGN); signal(SIGHUP, SIG_IGN); - signal(SIGTERM, handle_sigterm); /* Fork off for the second time*/ pid = fork(); @@ -840,15 +900,18 @@ int main(int argc, char** argv) } else { - - tcgetattr(0, &canonicalmode); - newsettings = canonicalmode; - newsettings.c_lflag &= (~ICANON & ~ECHO); - newsettings.c_cc[VMIN] = 1; - newsettings.c_cc[VTIME] = 0; - tcsetattr(0, TCSANOW, &newsettings); - char ch; - struct pollfd mypoll = { STDIN_FILENO, POLLIN|POLLPRI }; + stdin_is_tty = isatty(STDIN_FILENO); + if(stdin_is_tty) + { + tcgetattr(0, &canonicalmode); + newsettings = canonicalmode; + newsettings.c_lflag &= (~ICANON & ~ECHO); + newsettings.c_cc[VMIN] = 1; + newsettings.c_cc[VTIME] = 0; + tcsetattr(0, TCSANOW, &newsettings); + char ch; + struct pollfd mypoll = { STDIN_FILENO, POLLIN|POLLPRI }; + } } set_simapi_log_info(simapilib_loginfo); @@ -902,12 +965,6 @@ int main(int argc, char** argv) baton->req.data = (void*) baton; - uv_handle_set_data((uv_handle_t*) &gamefindtimer, (void*) baton); - uv_handle_set_data((uv_handle_t*) &bridgeclosetimer, (void*) baton); - uv_handle_set_data((uv_handle_t*) &datachecktimer, (void*) baton); - uv_handle_set_data((uv_handle_t*) &datamaptimer, (void*) baton); - uv_handle_set_data((uv_handle_t*) &recv_socket, (void*) baton); - appstate = 1; y_log_message(Y_LOG_LEVEL_DEBUG, "setting initial app state"); uv_timer_init(uv_default_loop(), &gamefindtimer); @@ -915,6 +972,11 @@ int main(int argc, char** argv) uv_timer_init(uv_default_loop(), &datachecktimer); uv_timer_init(uv_default_loop(), &datamaptimer); + uv_handle_set_data((uv_handle_t*) &gamefindtimer, (void*) baton); + uv_handle_set_data((uv_handle_t*) &bridgeclosetimer, (void*) baton); + uv_handle_set_data((uv_handle_t*) &datachecktimer, (void*) baton); + uv_handle_set_data((uv_handle_t*) &datamaptimer, (void*) baton); + y_log_message(Y_LOG_LEVEL_INFO, "Searching for sim data... Press q to quit...\n"); if(simds.auto_bridge == true) { @@ -927,17 +989,16 @@ int main(int argc, char** argv) uv_timer_start(&datachecktimer, datacheckcallback, 1000, 1000); } - uv_poll_t* poll; - if(simds.daemon == false) + if(simds.daemon == false && stdin_is_tty) { - uv_handle_set_data((uv_handle_t*) &pollt, (void*) baton); uv_poll_init(uv_default_loop(), &pollt, 0); + uv_handle_set_data((uv_handle_t*) &pollt, (void*) baton); uv_poll_start(&pollt, UV_READABLE, cb); } uv_run(uv_default_loop(), UV_RUN_DEFAULT); - if(simds.daemon==false) + if(simds.daemon==false && stdin_is_tty) { fflush(stdout); tcsetattr(0, TCSANOW, &canonicalmode);