/**
* Copyright (C) 2009-2014 Cars and Tracks Development Project (CTDP).
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package net.ctdp.rfdynhud.gamedata;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import net.ctdp.rfdynhud.input.InputAction;
import net.ctdp.rfdynhud.input.InputActionConsumer;
import net.ctdp.rfdynhud.input.__InpPrivilegedAccess;
import net.ctdp.rfdynhud.util.RFDHLog;
import org.jagatoo.util.xml.SimpleXMLHandler;
import org.jagatoo.util.xml.SimpleXMLParser;
import org.jagatoo.util.xml.SimpleXMLWriter;
import org.jagatoo.util.xml.XMLPath;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
class DataCache implements LiveGameData.GameDataUpdateListener, InputActionConsumer
{
private static final int CURRENT_VERSION_MAJOR = 1;
private static final int CURRENT_VERSION_MINOR = 2;
private static final int CURRENT_VERSION_REVISION = 0;
private static final String CURRENT_VERSION_STRING = CURRENT_VERSION_MAJOR + "." + CURRENT_VERSION_MINOR + "." + CURRENT_VERSION_REVISION;
@Override
public void onBoundInputStateChanged( InputAction action, boolean state, int modifierMask, long when, LiveGameData gameData, boolean isEditorMode )
{
if ( action == INPUT_ACTION_RESET_LAPTIMES_CACHE )
{
liveReset( gameData );
}
}
static final DataCache INSTANCE = new DataCache();
static final InputAction INPUT_ACTION_RESET_LAPTIMES_CACHE = __InpPrivilegedAccess.createInputAction( "ResetLaptimesCache", true, false, INSTANCE, DataCache.class.getClassLoader().getResource( DataCache.class.getPackage().getName().replace( '.', '/' ) + "/doc/ResetLaptimesCache.html" ) );
private static class VersionException extends RuntimeException
{
private static final long serialVersionUID = 1L;
public VersionException( String message )
{
super( message );
}
public static final void checkVersion( String version, int maxMajor, int maxMinor, int maxRevision ) throws VersionException
{
int[] ver = null;
try
{
String[] ss = version.split( "\\." );
ver = new int[ ss.length ];
for ( int i = 0; i < ss.length; i++ )
ver[i] = Integer.parseInt( ss[i] );
}
catch ( Throwable t )
{
throw new VersionException( "Error parsing version from configuration file." );
}
int[] testVer = { maxMajor, maxMinor, maxRevision };
boolean isNewer = false;
for ( int i = 0; i < ver.length && !isNewer; i++ )
{
int tv = ( testVer.length > i ) ? testVer[i] : 0;
if ( ver[i] > tv )
isNewer = true;
}
if ( isNewer )
throw new VersionException( "The cache file has a newer format than this version of rfDynHUD is able to handle." );
}
}
private final Map<String, Float> fuelUsages = new HashMap<String, Float>();
private final Map<String, Laptime> fastestNormalLaptimes = new HashMap<String, Laptime>();
private final Map<String, Laptime> fastestHotLaptimes = new HashMap<String, Laptime>();
void liveReset( LiveGameData gameData )
{
final SessionType sessionType = gameData.getScoringInfo().getSessionType();
Laptime.LapType lapType;
if ( sessionType.isRace() )
lapType = Laptime.LapType.RACE;
else if ( sessionType.isQualifying() )
lapType = Laptime.LapType.QUALIFY;
else if ( Laptime.isHotlap( gameData ) )
lapType = Laptime.LapType.HOTLAP;
else
lapType = Laptime.LapType.NORMAL;
if ( lapType == Laptime.LapType.HOTLAP )
{
fastestHotLaptimes.clear();
gameData.getScoringInfo().getPlayersVehicleScoringInfo().cachedFastestHotLaptime = null;
}
else
{
fastestNormalLaptimes.clear();
gameData.getScoringInfo().getPlayersVehicleScoringInfo().cachedFastestNormalLaptime = null;
}
}
static final boolean checkSessionType( ScoringInfo scoringInfo )
{
SessionType sessionType = scoringInfo.getSessionType();
if ( sessionType == SessionType.TEST_DAY )
return ( true );
if ( sessionType == SessionType.PRACTICE1 )
return ( scoringInfo.getNumVehicles() == 1 );
return ( false );
}
void addLaptime( ScoringInfo scoringInfo, String teamName, Laptime laptime )
{
if ( !checkSessionType( scoringInfo ) )
return;
if ( laptime.getLapTime() <= 0f )
return;
Laptime cached;
switch ( laptime.getType() )
{
case NORMAL:
cached = fastestNormalLaptimes.get( teamName );
if ( ( cached == null ) || ( laptime.getLapTime() < cached.getLapTime() ) )
fastestNormalLaptimes.put( teamName, laptime );
break;
case HOTLAP:
cached = fastestHotLaptimes.get( teamName );
if ( ( cached == null ) || ( laptime.getLapTime() < cached.getLapTime() ) )
fastestHotLaptimes.put( teamName, laptime );
break;
// We're not interested in other times.
}
}
final Float getFuelUsage( String teamName )
{
return ( fuelUsages.get( teamName ) );
}
private static File getCacheFile( GameFileSystem fileSystem, String modName, String trackName, boolean createFolder )
{
File cacheFolder = fileSystem.getCacheFolder();
if ( cacheFolder == null )
return ( null );
cacheFolder = new File( new File( cacheFolder, "data" ), modName );
if ( createFolder )
{
try
{
cacheFolder.mkdirs();
}
catch ( Throwable t )
{
RFDHLog.exception( "WARNING: Failed to create cache folder. Data-Cache deactivated." );
}
}
else if ( !cacheFolder.exists() )
{
return ( null );
}
return ( new File( cacheFolder, trackName + ".xml" ) );
}
private static File getCacheFile( LiveGameData gameData, boolean createFolder )
{
return ( getCacheFile( gameData.getFileSystem(), gameData.getModInfo().getName(), gameData.getScoringInfo().getTrackName(), createFolder ) );
}
private void loadFromCache( File cacheFile )
{
SimpleXMLHandler handler = new SimpleXMLHandler()
{
private String currentTeam = null;
@Override
protected void onElementStarted( XMLPath path, String name, Object object, Attributes attributes ) throws SAXException
{
if ( ( path.getLevel() == 0 ) && name.equals( "CachedData" ) )
{
VersionException.checkVersion( attributes.getValue( "version" ), CURRENT_VERSION_MAJOR, CURRENT_VERSION_MINOR, CURRENT_VERSION_REVISION );
}
else if ( path.isAt( false, "CachedData" ) && name.equals( "VehicleData" ) )
{
currentTeam = attributes.getValue( "vehicle" );
}
else if ( path.isAt( false, "CachedData", "VehicleData" ) )
{
if ( name.equals( "FuelUsage" ) )
{
try
{
float avgFuelUsage = Float.parseFloat( attributes.getValue( "average" ) );
fuelUsages.put( currentTeam, avgFuelUsage );
}
catch ( NumberFormatException e )
{
RFDHLog.exception( "WARNING: DataCache: Unable to parse value \"" + attributes.getValue( "average" ) + "\" to a float for fuel usage." );
}
}
else if ( name.equals( "FastestLap" ) )
{
try
{
String type = attributes.getValue( "type" );
float sector1 = Float.parseFloat( attributes.getValue( "sector1" ) );
float sector2 = Float.parseFloat( attributes.getValue( "sector2" ) );
float sector3 = Float.parseFloat( attributes.getValue( "sector3" ) );
float lap = Float.parseFloat( attributes.getValue( "lap" ) );
Laptime laptime = new Laptime( 0, 0, sector1, sector2, sector3, false, false, true );
laptime.laptime = lap;
try
{
laptime.setType( Laptime.LapType.valueOf( type ) );
}
catch ( Throwable t )
{
laptime.setType( Laptime.LapType.UNKNOWN );
}
switch ( laptime.getType() )
{
case NORMAL:
fastestNormalLaptimes.put( currentTeam, laptime );
break;
case HOTLAP:
fastestHotLaptimes.put( currentTeam, laptime );
break;
// We're not interested in other times.
}
}
catch ( NumberFormatException e )
{
RFDHLog.exception( "WARNING: DataCache: Unable to parse laptime." );
}
}
}
}
@Override
protected void onElementData( XMLPath path, Attributes attributes, char[] data, int start, int length ) throws SAXException
{
}
@Override
protected void onElementEnded( XMLPath path, String name, Object object ) throws SAXException
{
if ( ( path.getLevel() == 1 ) && name.equals( "VehicleData" ) )
{
currentTeam = null;
}
}
@Override
protected void onParsingException( XMLPath path, ExceptionSeverity severity, SAXParseException ex ) throws SAXException
{
RFDHLog.exception( "XML parsing exception at " + path.toString() );
RFDHLog.exception( ex );
}
};
try
{
SimpleXMLParser.parseXML( cacheFile, handler );
}
catch ( ParserConfigurationException e )
{
RFDHLog.exception( e );
}
catch ( SAXException e )
{
RFDHLog.exception( e );
}
catch ( IOException e )
{
RFDHLog.exception( e );
}
catch ( VersionException e )
{
RFDHLog.exception( "ERROR: " + e.getMessage() );
}
}
static Float loadFuelUsageFromCache( GameFileSystem fileSystem, final String modName, final String trackName, final String teamName )
{
File cacheFile = getCacheFile( fileSystem, modName, trackName, false );
if ( cacheFile == null )
return ( null );
final Float[] result = { null };
SimpleXMLHandler handler = new SimpleXMLHandler()
{
private String currentTeam = null;
@Override
protected void onElementStarted( XMLPath path, String name, Object object, Attributes attributes ) throws SAXException
{
if ( ( path.getLevel() == 0 ) && name.equals( "CachedData" ) )
{
VersionException.checkVersion( attributes.getValue( "version" ), CURRENT_VERSION_MAJOR, CURRENT_VERSION_MINOR, CURRENT_VERSION_REVISION );
}
else if ( path.isAt( false, "CachedData" ) && name.equals( "VehicleData" ) )
{
currentTeam = attributes.getValue( "vehicle" );
}
else if ( path.isAt( false, "CachedData", "VehicleData" ) && name.equals( "FuelUsage" ) )
{
try
{
float avgFuelUsage = Float.parseFloat( attributes.getValue( "average" ) );
if ( teamName.equals( currentTeam ) )
result[0] = avgFuelUsage;
}
catch ( NumberFormatException e )
{
RFDHLog.exception( "WARNING: DataCache: Unable to parse value \"" + attributes.getValue( "average" ) + "\" to a float for fuel usage." );
}
}
}
@Override
protected void onElementData( XMLPath path, Attributes attributes, char[] data, int start, int length ) throws SAXException
{
}
@Override
protected void onElementEnded( XMLPath path, String name, Object object ) throws SAXException
{
if ( ( path.getLevel() == 1 ) && name.equals( "VehicleData" ) )
{
currentTeam = null;
}
}
@Override
protected void onParsingException( XMLPath path, ExceptionSeverity severity, SAXParseException ex ) throws SAXException
{
RFDHLog.exception( "XML parsing exception at " + path.toString() );
RFDHLog.exception( ex );
}
};
try
{
SimpleXMLParser.parseXML( cacheFile, handler );
}
catch ( ParserConfigurationException e )
{
RFDHLog.exception( e );
}
catch ( SAXException e )
{
RFDHLog.exception( e );
}
catch ( IOException e )
{
RFDHLog.exception( e );
}
catch ( VersionException e )
{
RFDHLog.exception( "Error: " + e.getMessage() );
}
return ( result[0] );
}
@Override
public void onSessionStarted( LiveGameData gameData, boolean isEditorMode )
{
fuelUsages.clear();
fastestNormalLaptimes.clear();
fastestHotLaptimes.clear();
VehicleScoringInfo player = gameData.getScoringInfo().getPlayersVehicleScoringInfo();
player.cachedFastestNormalLaptime = null;
player.cachedFastestHotLaptime = null;
File cacheFile = getCacheFile( gameData, false );
if ( ( cacheFile == null ) || !cacheFile.exists() )
return;
loadFromCache( cacheFile );
if ( !checkSessionType( gameData.getScoringInfo() ) )
return;
player.cachedFastestNormalLaptime = fastestNormalLaptimes.get( gameData.getProfileInfo().getTeamName() );
player.cachedFastestHotLaptime = fastestHotLaptimes.get( gameData.getProfileInfo().getTeamName() );
}
@Override
public void onCockpitEntered( LiveGameData gameData, boolean isEditorMode ) {}
@Override
public void onGamePauseStateChanged( LiveGameData gameData, boolean isEditorMode, boolean isPaused ) {}
private void storeToCache( LiveGameData gameData )
{
File cacheFile = getCacheFile( gameData, true );
if ( cacheFile == null )
return;
ArrayList<String> vehicleNames = new ArrayList<String>( fuelUsages.keySet() );
Collections.sort( vehicleNames );
SimpleXMLWriter writer = null;
try
{
writer = new SimpleXMLWriter( cacheFile );
writer.writeElementAndPush( "CachedData", "version", CURRENT_VERSION_STRING );
for ( String vehicleName : vehicleNames )
{
writer.writeElementAndPush( "VehicleData", "vehicle", vehicleName );
Float fuelUsage = fuelUsages.get( vehicleName );
if ( fuelUsage != null )
writer.writeElement( "FuelUsage", "average", fuelUsage );
Laptime laptime = fastestNormalLaptimes.get( vehicleName );
if ( laptime != null )
writer.writeElement( "FastestLap", "type", laptime.getType().name(), "sector1", laptime.getSector1(), "sector2", laptime.getSector2(), "sector3", laptime.getSector3(), "lap", laptime.getLapTime() );
laptime = fastestHotLaptimes.get( vehicleName );
if ( laptime != null )
writer.writeElement( "FastestLap", "type", laptime.getType().name(), "sector1", laptime.getSector1(), "sector2", laptime.getSector2(), "sector3", laptime.getSector3(), "lap", laptime.getLapTime() );
writer.popElement();
}
writer.popElement();
}
catch ( IOException e )
{
RFDHLog.exception( e );
}
catch ( SAXException e )
{
RFDHLog.exception( e );
}
finally
{
try { writer.close(); } catch ( Throwable t ) {}
}
}
@Override
public void onCockpitExited( LiveGameData gameData, boolean isEditorMode )
{
//VehicleScoringInfo player = gameData.getScoringInfo().getPlayersVehicleScoringInfo();
//String teamName = player.getVehicleName();
String teamName = gameData.getProfileInfo().getTeamName();
float avgFuelUsage = FuelUsageRecorder.MASTER_FUEL_USAGE_RECORDER.getAverage();
if ( avgFuelUsage > 0f )
fuelUsages.put( teamName, avgFuelUsage );
//fastestLaptimes.put( teamName, player.getFastestLaptime() );
storeToCache( gameData );
}
private DataCache()
{
}
}