/** * @see \Simresults\Data_Reader::readSessions() */ protected function readSessions() { // Get data $data = json_decode(self::cleanJSON($this->data), TRUE); $data = $data['stats']; // No session data if (!($history_data = $data['history'])) { // Throw exception throw new Exception\Reader('Cannot read the session data'); } // Remove sessions without stages $history_data = array_filter($history_data, function ($data) { return (bool) $data['stages']; }); // Get attribute info of project cars to figure out vehicle names etc $this->attribute_names = $this->getAttributeNames(); // Init sessions array $sessions = array(); // Loop each history item foreach ($history_data as $history) { /** * Collect all participants */ $initial_participants_by_ref = array(); // Depricated! TODO: Remove $initial_participants_by_id = array(); // Loop all member entries and create participants foreach ($history['members'] as $part_ref => $part_data) { // Get participant $participant = $this->getParticipant($part_data); // Add participant to collection // $initial_participants_by_ref[$part_ref] = $participant; $initial_participants_by_id[$part_data['participantid']] = $participant; } // Get additional info from participants entries // Disabled due to duplicate refids bugs 2015-12-14 // foreach ($history['participants'] as $part_data) // { // // Get previously parsed participant // $participant = $initial_participants_by_ref[ // $part_data['RefId']]; // // Set whether participant is human // $participant->getDriver()->setHuman( // (bool) $part_data['IsPlayer']); // } // Get server configuration $read_settings = array('DamageType', 'FuelUsageType', 'PenaltiesType', 'ServerControlsSetup', 'ServerControlsTrack', 'ServerControlsVehicle', 'ServerControlsVehicleClass', 'TireWearType'); $session_settings = array(); foreach ($history['setup'] as $setup_key => $setup_value) { if (in_array($setup_key, $read_settings)) { $session_settings[$setup_key] = $setup_value; } } // Loop all stages data foreach ($history['stages'] as $type_key => $session_data) { // Make new unique array of participants to prevent reference // issues across multiple sessions $participants_by_ref = array(); $participants_by_id = array(); foreach ($initial_participants_by_ref as $part_key => $part) { $participants_by_ref[$part_key] = clone $part; } foreach ($initial_participants_by_id as $part_key => $part) { $participants_by_id[$part_key] = clone $part; } // Init session $session = Session::createInstance(); // Practice session by default $type = Session::TYPE_PRACTICE; // Setup name for session type $type_setup_name = ucfirst($type_key); // Check session name to get type // TODO: Could we prevent duplicate code for this with other readers? switch (strtolower(preg_replace('#\\d#', '', $type_key))) { case 'qualifying': $type = Session::TYPE_QUALIFY; $type_setup_name = 'Qualify'; break; case 'warmup': $type = Session::TYPE_WARMUP; break; case 'race': $type = Session::TYPE_RACE; break; } // Date of this session $date = new \DateTime(); $date->setTimestamp($session_data['start_time']); $date->setTimezone(new \DateTimeZone(self::$default_timezone)); // Set session values $session->setType($type)->setName($type_key)->setMaxLaps($history['setup'][$type_setup_name . 'Length'])->setDate($date)->setOtherSettings($session_settings); // Set game $game = new Game(); $game->setName('Project Cars'); $session->setGame($game); // Set server // TODO: Set configurations $server = new Server(); $server->setName($data['server']['name']); $session->setServer($server); // Set track $track = new Track(); // Have friendly track name if (isset($this->attribute_names['tracks'][$history['setup']['TrackId']])) { $track->setVenue($this->attribute_names['tracks'][$history['setup']['TrackId']]['name']); } else { // TODO: We should test this works too? Same for vehicles // when our json attribute config is missing items $track->setVenue((string) $history['setup']['TrackId']); } $session->setTrack($track); // Remember participants with actual events $participants_with_events = array(); // Parse events first only to collect missing participants foreach ($session_data['events'] as $event) { // Participant unknown if (!isset($participants_by_id[$event['participantid']])) { // Build it and fallback to less info $part = $this->getParticipant($event); $participants_by_id[$event['participantid']] = $part; } } // Parse event data such as laps $cut_data = array(); foreach ($session_data['events'] as $event) { // Get participant $part = $participants_by_id[$event['participantid']]; // Remember this participant $participants_with_events[] = $part; // Is lap and the lap is valid (only checked for non-race) if ($event['event_name'] === 'Lap' and ($session->getType() === Session::TYPE_RACE or $event['attributes']['CountThisLapTimes'])) { // Init new lap $lap = new Lap(); // Set participant $lap->setParticipant($part); // Set first driver of participant as lap driver. PJ // does not support swapping $lap->setDriver($part->getDriver()); // Set total time $lap->setTime(round($event['attributes']['LapTime'] / 1000, 4)); // Add sectors for ($sector_i = 1; $sector_i <= 3; $sector_i++) { $lap->addSectorTime(round($event['attributes']['Sector' . $sector_i . 'Time'] / 1000, 4)); } // Set lap position $lap->setPosition($event['attributes']['RacePosition']); // Set number $lap->setNumber($event['attributes']['Lap'] + 1); // Add lap to participant $part->addLap($lap); } elseif ($event['event_name'] === 'Impact') { // Other participant is unknown by default $other_participant_name = 'unknown'; // Other participant known if (-1 != ($other_id = $event['attributes']['OtherParticipantId']) and isset($participants_by_id[$other_id])) { // Set other name $other_participant_name = $participants_by_id[$other_id]->getDriver()->getName(); } else { // Skip for now until we know what -1 means continue; } $incident = new Incident(); $incident->setMessage(sprintf('%s reported contact with another vehicle ' . '%s. CollisionMagnitude: %s', $part->getDriver()->getName(), $other_participant_name, $event['attributes']['CollisionMagnitude'])); // TODO: Add elapsed time $date = new \DateTime(); $date->setTimestamp($event['time']); $incident->setDate($date); $incident->setElapsedSeconds($date->getTimestamp() - $session->getDate()->getTimestamp()); $session->addIncident($incident); } elseif (in_array($event['event_name'], array('CutTrackStart', 'CutTrackEnd'))) { $cut_data[] = $event; } elseif ($event['event_name'] === 'State' and $event['attributes']['NewState'] === 'Retired') { $part->setFinishStatus(Participant::FINISH_DNF); } } /** * TODO: So many duplicate code below regarding results array * reading! Fix this */ // Has results array we can read finish statusses from if ($results = $session_data['results']) { // Loop each result and process the lap foreach ($results as $result) { // Participant not found, continue to next if (!isset($participants_by_id[$result['participantid']])) { continue; } // Has DNF state if (in_array(strtolower($result['attributes']['State']), array('dnf', 'retired'))) { // Get participant $part = $participants_by_id[$result['participantid']]; // Set DNF $part->setFinishStatus(Participant::FINISH_DNF); } } } // We did not have any events data to process but we have // final results. Let's use this data to atleast get 1 best // lap of these participants if (!$session_data['events'] and $results = $session_data['results']) { // Loop each result and process the lap foreach ($results as $result) { // Participant not found, continue to next if (!isset($participants_by_id[$result['participantid']])) { continue; } // Get participant $part = $participants_by_id[$result['participantid']]; // Remember this participant (fake it had events) $participants_with_events[] = $part; // Has best lap if ($result['attributes']['FastestLapTime']) { // Init new lap $lap = new Lap(); // Set participant $lap->setParticipant($part); // Set first driver of participant as lap driver. PJ // does not support swapping $lap->setDriver($part->getDriver()); // Set total time $lap->setTime(round($result['attributes']['FastestLapTime'] / 1000, 4)); // Set number $lap->setNumber(1); // Add lap to participant $part->addLap($lap); } } } /** * Process cut info */ foreach ($cut_data as $key => $event) { // Get participant $part = $participants_by_id[$event['participantid']]; // Start of cut and lap actually exists if ($event['event_name'] === 'CutTrackStart' and $lap = $part->getLap($event['attributes']['Lap'] + 1)) { // Find the end of cutting by looping following events for ($end_key = $key + 1; $end_key < count($cut_data); $end_key++) { $next_event = $cut_data[$end_key]; // Next event is another cut start. Ignore current // cut as theres no proper end data if ($next_event['event_name'] === 'CutTrackStart' and $next_event['participantid'] == $event['participantid']) { // Theres no end break; } elseif ($next_event['event_name'] === 'CutTrackEnd' and $next_event['participantid'] == $event['participantid']) { $cut = new Cut(); $cut->setCutTime(round($next_event['attributes']['ElapsedTime'] / 1000, 4)); $cut->setTimeSkipped(round($next_event['attributes']['SkippedTime'] / 1000, 4)); $date = new \DateTime(); $date->setTimestamp($next_event['time']); $date->setTimezone(new \DateTimeZone(self::$default_timezone)); $cut->setDate($date); $cut->setLap($lap); $cut->setElapsedSeconds($date->getTimestamp() - $session->getDate()->getTimestamp()); $cut->setElapsedSecondsInLap(round($event['attributes']['LapTime'] / 1000, 4)); $lap->addCut($cut); // Stop searching break; } } } } /** * Cleanup */ $participants = $participants_by_id; // Remove any participant who did not participate foreach ($participants as $part_id => $part) { // No laps and not marked as participated // TODO: Make test for strict comparison (true arg), log // is on Project Cars forum for test if (!in_array($part, $participants_with_events, true)) { unset($participants[$part_id]); } } // Get participant with normal array keys $participants = array_values($participants); // Session has predefined race result positions // WARNING: We only do this for race sessions because for // qualify and practice some drivers are missing from the // result if ($results = $session_data['results'] and $session->getType() === Session::TYPE_RACE) { // Sort participants using our own sort $tmp_sort = Helper::sortParticipantsByTotalTime($participants); // Find whether our leading participant using our own sort // is in the result $leading_is_in_result = false; foreach ($results as $result) { // Participant not found, continue to next if (!isset($participants_by_id[$result['participantid']])) { continue; } // Get participant $participant = $participants_by_id[$result['participantid']]; // Leading found if ($participant === $tmp_sort[0]) { $leading_is_in_result = true; } } // Leading participant is in the results array if ($leading_is_in_result) { // Init sorted result array $participants_resultsorted = array(); foreach ($results as $result) { // Participant not found, continue to next if (!isset($participants_by_id[$result['participantid']])) { continue; } // Get participant $participant = $participants_by_id[$result['participantid']]; // Set total time $participant->setTotalTime(round($result['attributes']['TotalTime'] / 1000, 4)); // Add to sorted array and remove from normal array $participants_resultsorted[] = $participant; unset($participants[array_search($participant, $participants, true)]); } // Sort participants not sorted by result by total time $participants = Helper::sortParticipantsByTotalTime($participants); // Merge the sorted participants result with normal sort // array. Merge them and remove any duplicates // NOTE: We are not using array_unique as it's causing // recursive depedency $merged = array_merge($participants_resultsorted, $participants); $final = array(); foreach ($merged as $current) { if (!in_array($current, $final, true)) { $final[] = $current; } } $participants = $final; } else { // Sort participants $this->sortParticipantsAndFixPositions($participants, $session); } } else { // Is race if ($session->getType() === Session::TYPE_RACE) { // Set all participants on unknown finish status // We should of had a result for proper statusses foreach ($participants as $part) { $part->setFinishStatus(Participant::FINISH_NONE); } } // Sort participants $this->sortParticipantsAndFixPositions($participants, $session, TRUE); } // Set participants (sorted) $session->setParticipants($participants); $sessions[] = $session; } // End stages loop } // End history loop // Swap warmup and race positions if wrong $prevous_session = null; foreach ($sessions as $key => $session) { // Found warmup after race session if ($prevous_session and $prevous_session->getType() === Session::TYPE_RACE and $session->getType() === Session::TYPE_WARMUP) { // Swap them $sessions[$key] = $prevous_session; $sessions[$key - 1] = $session; } // Remember previous session $prevous_session = $session; } // Return sessions return $sessions; }
/** * Sets the incidents on a session instance * * @param Sesssion $session */ protected function setIncidents(Session $session) { // No incidents by default $incidents = array(); // Get incidents from XML $incidents_dom = $this->dom->getElementsByTagName('Incident'); // Way to many incidents! if ($incidents_dom->length > 2000) { // Create new dummy incident $incident = new Incident(); $session->setIncidents(array($incident->setMessage('Sorry, way too many incidents to show!')->setDate(clone $session->getDate()))); return; } // Loop each incident (if any) /* @var $incident_xml \DOMNode */ foreach ($incidents_dom as $incident_xml) { // Create new incident $incident = new Incident(); // Set message $incident->setMessage($incident_xml->nodeValue); // Clone session date $date = clone $session->getDate(); // Add the seconds to date, ignoring any decimals $date->modify(sprintf('+%d seconds', (int) $incident_xml->getAttribute('et'))); // Set real estimated seconds $incident->setElapsedSeconds((double) $incident_xml->getAttribute('et')); // Add date to incident $incident->setDate($date); // Is incident with another vehicle if (strpos(strtolower($incident->getMessage()), 'with another vehicle')) { // Match impact preg_match('/reported contact \\((.*)\\) with another vehicle/i', $incident->getMessage(), $matches); // Worth reviewing when impact is >= 60% $incident->setForReview(isset($matches[1]) and (double) $matches[1] >= 0.6); } // Add incident to incidents $incidents[] = $incident; } // Set incidents on session $session->setIncidents($incidents); }