├── .gitignore ├── src └── main │ ├── resources │ └── icon.png │ └── java │ └── de │ └── reilem │ └── replaychart │ ├── E_SteeringType.java │ ├── gbx │ ├── E_TmVersion.java │ ├── GbxSteeringInput.java │ ├── E_GbxInputType.java │ ├── GbxReplayBuilder.java │ └── GbxInputExtractor.java │ ├── ReplayTheme.java │ ├── SteeringAction.java │ ├── ReplayData.java │ ├── MainWindow.java │ ├── donadigo │ └── DonadigoReplayBuilder.java │ └── ReplayChart.java ├── readme.md ├── pom.xml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | .classpath 4 | /target/ 5 | .idea 6 | *.iml 7 | /replay_inputs/ 8 | -------------------------------------------------------------------------------- /src/main/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railem/replayChart/HEAD/src/main/resources/icon.png -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/E_SteeringType.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart; 2 | 3 | public enum E_SteeringType 4 | { 5 | DIGITAL, ANALOG; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/gbx/E_TmVersion.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart.gbx; 2 | 3 | public enum E_TmVersion 4 | { 5 | ESWC, FOREVER, TM2; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/ReplayTheme.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart; 2 | 3 | import org.knowm.xchart.style.colors.ChartColor; 4 | import org.knowm.xchart.style.theme.XChartTheme; 5 | 6 | import java.awt.*; 7 | 8 | public class ReplayTheme extends XChartTheme 9 | { 10 | public Color getChartBackgroundColor() { 11 | return ChartColor.getAWTColor(ChartColor.LIGHT_GREY); 12 | } 13 | public Color getPlotGridLinesColor() { 14 | return ChartColor.getAWTColor(ChartColor.WHITE); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/SteeringAction.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart; 2 | 3 | public class SteeringAction 4 | { 5 | String time; 6 | String value; 7 | 8 | public SteeringAction( String time, String value ) 9 | { 10 | this.time = time; 11 | this.value = value; 12 | } 13 | 14 | public String getTime() 15 | { 16 | return time; 17 | } 18 | 19 | public void setTime( String time ) 20 | { 21 | this.time = time; 22 | } 23 | 24 | public String getValue() 25 | { 26 | return value; 27 | } 28 | 29 | public void setValue( String value ) 30 | { 31 | this.value = value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/gbx/GbxSteeringInput.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart.gbx; 2 | 3 | public class GbxSteeringInput 4 | { 5 | public static double MAX = 65536.0; 6 | public static double MIN = -65536.0; 7 | 8 | private int time; 9 | private E_GbxInputType type; 10 | private int value; 11 | 12 | public int getTime() 13 | { 14 | return time; 15 | } 16 | 17 | public void setTime( int time ) 18 | { 19 | this.time = time; 20 | } 21 | 22 | public E_GbxInputType getType() 23 | { 24 | return type; 25 | } 26 | 27 | public void setType( E_GbxInputType type ) 28 | { 29 | this.type = type; 30 | } 31 | 32 | public int getValue() 33 | { 34 | return value; 35 | } 36 | 37 | public void setValue( int value ) 38 | { 39 | this.value = value; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/gbx/E_GbxInputType.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart.gbx; 2 | 3 | public enum E_GbxInputType 4 | { 5 | START, ACCELERATE, BRAKE, STEER, STEER_RIGHT, STEER_LEFT, FINISH, RESPAWN, UNKNOWN; 6 | 7 | public static E_GbxInputType getType( String controlName ) 8 | { 9 | switch ( controlName ) 10 | { 11 | case "_FakeIsRaceRunning": 12 | return START; 13 | case "Accelerate": 14 | return ACCELERATE; 15 | case "Brake": 16 | return BRAKE; 17 | case "SteerRight": 18 | return STEER_RIGHT; 19 | case "SteerLeft": 20 | return STEER_LEFT; 21 | case "Steer": 22 | return STEER; 23 | case "_FakeFinishLine": 24 | return FINISH; 25 | case "Respawn": 26 | return RESPAWN; 27 | default: 28 | return UNKNOWN; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Replay Chart 2 | 3 | **Replay Chart** is a Java Tool that can visualize TrackMania steering, throttle and brake inputs in charts. 4 | It can visualize multiple replays at once, either individually (each in separate charts) or overlapped (all in the same chart). 5 |
Both pad and keyboard runs can be processed and will be marked accordingly in the chart. 6 | The percentage behind the input type shows how much of the run is driven on with that type (Analog/Digital). 7 |
The Tool also displays the time of each run above the chart, as well as the percentage of the run spend pressing the brake and throttle! 8 | 9 | The tool needs no external dependencies to function!
10 | With the help of lx and donadigo the tool now extracts the input of each replay itself! 11 | 12 | Supported TrackMania version: TMO, TMS, TMN ESWC, TMNF, TMUF, TM2 13 | 14 | ### [Download the latest Release [v1.4]](https://github.com/railem/replayChart/releases/download/1.4/replayChart-1.4.jar) 15 | 16 | ## Tool Usage 17 | In order to run the Tool you need to either run it via double-click or execute it via `java -jar replayChart-1.4.jar`.
18 | You can then select the replays you want to analyze. 19 | 20 | 21 | ## Example 1 (TMNF - D07-Race) 22 | 23 | ![](https://i.imgur.com/OQEQ0Kx.png"") 24 | 25 | ## Example 2 (TMN ESWC - reRun) 26 | 27 | ![](https://i.imgur.com/8R6Zzpc.png"") 28 | 29 | ## Commandline 30 | The tool still supports the commandline variant. Just execute the jar with the extra parameters. 31 | 32 | `-o` - Overlays the steering of multiple replays in one chart.
33 | Acceleration and brake, and other individual stats will not be displayed in this mode. 34 |

35 | `-i` - Inverts left and right.
36 | Might help with orientation when following the timeline. 37 |

38 | `-m` - Match Timeline.
39 | Matches the timeline of all replays to improve the comparability. 40 |

41 | `/path/to/file` - path to folder or replay to analyze.
42 | Can be used multiple times to add more than one file. 43 | 44 | ## Changelog 45 | 46 | ### 1.1 47 | - added brake and throttle graphs to chart 48 | - improved legend & labels 49 | - renamed -overlay parameter to (-o) 50 | - added zoom feature 51 | - added invert parameter (-i) 52 | 53 | ### 1.2 54 | - made the graph calculation more accurate 55 | - added replay time display 56 | - moved legend into the chart on overlay mode 57 | - added label showing the time spend pressing acceleration & brake in percent 58 | - added custom theme for a clean for a look 59 | - drawing custom lines to help orientation 60 | - removed broken zoom feature 61 | 62 | ### 1.3 63 | - showing the percentage of the run driven on each device 64 | - added support for replay.gbx input extraction 65 | - added gui 66 | 67 | ### 1.4 68 | - added match timeline (-m) feature 69 | - improved respawn marker 70 | - improved display of the race time 71 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.example 8 | replayChart 9 | 1.4 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | org.knowm.xchart 19 | xchart 20 | 3.8.0 21 | 22 | 23 | org.anarres.lzo 24 | lzo-core 25 | 1.0.6 26 | 27 | 28 | commons-codec 29 | commons-codec 30 | 1.15 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-assembly-plugin 39 | 40 | 41 | jar-with-dependencies 42 | 43 | ${project.artifactId}-${project.version} 44 | false 45 | 46 | 47 | de.reilem.replaychart.MainWindow 48 | 49 | 50 | 51 | 52 | 53 | package 54 | 55 | single 56 | 57 | 58 | 59 | 60 | de.reilem.replaychart.MainWindow 61 | 62 | 63 | 64 | jar-with-dependencies 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/gbx/GbxReplayBuilder.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart.gbx; 2 | 3 | import de.reilem.replaychart.ReplayData; 4 | 5 | import java.util.List; 6 | 7 | public class GbxReplayBuilder 8 | { 9 | private int timestamp = 0; 10 | private double acceleration = 0.0; 11 | private double brake = 0.0; 12 | private double steering = 0.0; //pad 13 | private double steer_right = 0.0; //kb 14 | private double steer_left = 0.0; //kb 15 | 16 | private List inputs; 17 | private ReplayData replay; 18 | 19 | public ReplayData build( int replayTime, List inputs, boolean invertedSteering, 20 | E_TmVersion tmVersion ) 21 | { 22 | this.inputs = inputs; 23 | replay = new ReplayData(); 24 | replay.setReplayTime( replayTime ); 25 | 26 | while ( timestamp < replayTime ) 27 | { 28 | evaluateEventsAt( timestamp, invertedSteering ); 29 | addTimeStamp(); 30 | timestamp += 10; 31 | } 32 | 33 | for ( GbxSteeringInput input : inputs ) 34 | { 35 | if ( input.getType() == E_GbxInputType.RESPAWN && input.getValue() == 1 ) 36 | { 37 | replay.addRespawn( input.getTime() ); 38 | } 39 | } 40 | 41 | return replay; 42 | } 43 | 44 | private void evaluateEventsAt( int timestamp, boolean invertedSteering ) 45 | { 46 | for ( GbxSteeringInput input : inputs ) 47 | { 48 | if ( input.getTime() == timestamp ) 49 | { 50 | if ( input.getType() == E_GbxInputType.ACCELERATE ) 51 | { 52 | acceleration = input.getValue() > 0.0 ? GbxSteeringInput.MAX : 0.0; 53 | } 54 | else if ( input.getType() == E_GbxInputType.BRAKE ) 55 | { 56 | brake = input.getValue() > 0.0 ? GbxSteeringInput.MIN : 0.0; 57 | } 58 | else if ( input.getType() == E_GbxInputType.STEER_RIGHT ) 59 | { 60 | steer_right = input.getValue() == 0.0 ? 0.0 : invertedSteering ? GbxSteeringInput.MIN : GbxSteeringInput.MAX; 61 | } 62 | else if ( input.getType() == E_GbxInputType.STEER_LEFT ) 63 | { 64 | steer_left = input.getValue() == 0.0 ? 0.0 : invertedSteering ? GbxSteeringInput.MAX : GbxSteeringInput.MIN; 65 | } 66 | else if ( input.getType() == E_GbxInputType.STEER ) 67 | { 68 | steering = invertedSteering ? input.getValue() * -1.0 : input.getValue(); 69 | } 70 | } 71 | } 72 | } 73 | 74 | private void addTimeStamp() 75 | { 76 | replay.addTimestamp( timestamp ); 77 | replay.addAcceleration( acceleration ); 78 | replay.addBrake( brake ); 79 | 80 | if ( acceleration != 0.0 ) 81 | { 82 | replay.addThrottleAction(); 83 | } 84 | if ( brake != 0.0 ) 85 | { 86 | replay.addBrakeAction(); 87 | } 88 | 89 | if ( steering != 0.0 ) //define that pad steering is stronger than keyboard 90 | { 91 | replay.addSteering( steering ); 92 | replay.addPadAction(); 93 | } 94 | else if ( steer_left != 0.0 ) //left is stronger than right 95 | { 96 | replay.addSteering( steer_left ); 97 | replay.addKeyboardAction(); 98 | } 99 | else if ( steer_right != 0.0 ) 100 | { 101 | replay.addSteering( steer_right ); 102 | replay.addKeyboardAction(); 103 | } 104 | else 105 | { 106 | replay.addSteering( 0.0 ); 107 | } 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/ReplayData.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart; 2 | 3 | import de.reilem.replaychart.gbx.E_TmVersion; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Holds one replays steering movement for each timestamp 10 | */ 11 | public class ReplayData 12 | { 13 | private String fileName; 14 | private E_SteeringType type; 15 | private int replayTime; 16 | private E_TmVersion tmVersion; 17 | 18 | private List steering; 19 | private List acceleration; 20 | private List brake; 21 | private List timestamps; 22 | private List respawns; 23 | 24 | private int timeOnThrottle; 25 | private int timeOnBrake; 26 | private int keyboardSteers; 27 | private int padSteers; 28 | 29 | public ReplayData() 30 | { 31 | steering = new ArrayList<>(); 32 | acceleration = new ArrayList<>(); 33 | brake = new ArrayList<>(); 34 | timestamps = new ArrayList<>(); 35 | respawns = new ArrayList<>(); 36 | timeOnThrottle = 0; 37 | timeOnBrake = 0; 38 | keyboardSteers = 0; 39 | padSteers = 0; 40 | } 41 | 42 | public int getSteeringLegth() 43 | { 44 | return steering.size(); 45 | } 46 | 47 | public double[] getSteering() 48 | { 49 | return steering.isEmpty() ? null : listToArray( steering ); 50 | } 51 | 52 | public double[] getTimestamps() 53 | { 54 | return timestamps.isEmpty() ? null : listToArray( timestamps ); 55 | } 56 | 57 | public void addSteering( double d ) 58 | { 59 | steering.add( d ); 60 | } 61 | 62 | public void addTimestamp( double d ) 63 | { 64 | timestamps.add( d ); 65 | } 66 | 67 | public String getChartTitle() 68 | { 69 | return fileName; 70 | } 71 | 72 | public String getChartTitleShort() 73 | { 74 | return fileName + " [" + type.name() + "] [" + ReplayChart.formatTime( (double) replayTime, tmVersion ) + "]"; 75 | } 76 | 77 | public void setFileName( String name ) 78 | { 79 | this.fileName = name; 80 | } 81 | 82 | public void setType( E_SteeringType type ) 83 | { 84 | this.type = type; 85 | } 86 | 87 | public double[] getAcceleration() 88 | { 89 | return acceleration.isEmpty() ? null : listToArray( acceleration ); 90 | } 91 | 92 | public double[] getBrake() 93 | { 94 | return brake.isEmpty() ? null : listToArray( brake ); 95 | } 96 | 97 | public void addAcceleration( double d ) 98 | { 99 | acceleration.add( d ); 100 | } 101 | 102 | public void addBrake( double d ) 103 | { 104 | brake.add( d ); 105 | } 106 | 107 | public int getReplayTime() 108 | { 109 | return replayTime; 110 | } 111 | 112 | public void setReplayTime( int replayTime ) 113 | { 114 | this.replayTime = replayTime; 115 | } 116 | 117 | public String getType() 118 | { 119 | if ( padSteers > keyboardSteers ) 120 | { 121 | type = E_SteeringType.DIGITAL; 122 | return type + " (" + ReplayChart.roundDoubleTwoDecimalPlaces( 123 | ((double) padSteers / (double) (padSteers + keyboardSteers)) * 100 ) + "%)"; 124 | } 125 | else 126 | { 127 | type = E_SteeringType.ANALOG; 128 | return type + " (" + ReplayChart.roundDoubleTwoDecimalPlaces( 129 | ((double) keyboardSteers / (double) (padSteers + keyboardSteers)) * 100 ) + "%)"; 130 | } 131 | } 132 | 133 | public int getTimeOnThrottle() 134 | { 135 | return timeOnThrottle; 136 | } 137 | 138 | public int getTimeOnBrake() 139 | { 140 | return timeOnBrake; 141 | } 142 | 143 | public void addThrottleAction() 144 | { 145 | timeOnThrottle++; 146 | } 147 | 148 | public void addBrakeAction() 149 | { 150 | timeOnBrake++; 151 | } 152 | 153 | public void addPadAction() 154 | { 155 | padSteers++; 156 | } 157 | 158 | public void addKeyboardAction() 159 | { 160 | keyboardSteers++; 161 | } 162 | 163 | public List getRespawns() 164 | { 165 | return respawns; 166 | } 167 | 168 | public void addRespawn( int time ) 169 | { 170 | respawns.add( time ); 171 | } 172 | 173 | public E_TmVersion getTmVersion() 174 | { 175 | return tmVersion; 176 | } 177 | 178 | public void setTmVersion( E_TmVersion tmVersion ) 179 | { 180 | this.tmVersion = tmVersion; 181 | } 182 | 183 | public String getPercentTimeOnThrottle() 184 | { 185 | return "Throttle: [" + ReplayChart.roundDoubleTwoDecimalPlaces( ((double) (timeOnThrottle * 10) / (double) replayTime) * 100 ) 186 | + "%]"; 187 | } 188 | 189 | public String getPercentTimeOnBrake() 190 | { 191 | return "Brake: [" + ReplayChart.roundDoubleTwoDecimalPlaces( ((double) (timeOnBrake * 10) / (double) replayTime) * 100 ) + "%]"; 192 | } 193 | 194 | public static double[] listToArray( List list ) 195 | { 196 | double[] array = new double[list.size()]; 197 | for ( int i = 0; i < array.length; i++ ) 198 | { 199 | array[i] = list.get( i ); 200 | } 201 | return array; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/MainWindow.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart; 2 | 3 | import javax.imageio.ImageIO; 4 | import javax.swing.*; 5 | import javax.swing.filechooser.FileFilter; 6 | import java.awt.*; 7 | import java.io.File; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | public class MainWindow extends javax.swing.JFrame 13 | { 14 | public static void main( String[] args ) 15 | { 16 | if ( args.length == 0 ) //show UI 17 | { 18 | MainWindow window = new MainWindow(); 19 | window.start(); 20 | } 21 | else 22 | { 23 | List arguments = new ArrayList<>( Arrays.asList( args ) ); 24 | boolean overlaySteering = false; 25 | boolean invertSteering = false; 26 | boolean matchTimeline = false; 27 | 28 | if ( arguments.contains( "-o" ) ) //check for overlay mode 29 | { 30 | overlaySteering = true; 31 | arguments.remove( "-o" ); 32 | } 33 | if ( arguments.contains( "-i" ) ) //check for inverted steering mode 34 | { 35 | invertSteering = true; 36 | arguments.remove( "-i" ); 37 | } 38 | if ( arguments.contains( "-m" ) ) //check for inverted steering mode 39 | { 40 | matchTimeline = true; 41 | arguments.remove( "-m" ); 42 | } 43 | 44 | new ReplayChart().init( arguments, overlaySteering, invertSteering, false, matchTimeline ); 45 | } 46 | } 47 | 48 | private JCheckBox overlayCb; 49 | private JCheckBox invertCb; 50 | private JCheckBox matchTimelineCb; 51 | 52 | private void start() 53 | { 54 | try 55 | { 56 | this.setIconImage( ImageIO.read( 57 | Thread.currentThread().getContextClassLoader().getResourceAsStream( "icon.png" ) ) ); 58 | } 59 | catch ( Throwable e ) 60 | { 61 | //ignore it 62 | } 63 | this.setTitle( "ReplayChart v1.4" ); 64 | this.setSize( 500, 210 ); 65 | this.setResizable( false ); 66 | centerWindow(); 67 | this.setVisible( true ); 68 | this.setDefaultCloseOperation( WindowConstants.EXIT_ON_CLOSE ); 69 | JPanel panel = new JPanel(); 70 | panel.setLayout( null ); 71 | this.add( panel ); 72 | 73 | JFileChooser fc = new JFileChooser(); 74 | fc.setMultiSelectionEnabled( true ); 75 | fc.setFileFilter( new FileFilter() 76 | { 77 | @Override public boolean accept( File f ) 78 | { 79 | if ( f.isDirectory() ) 80 | { 81 | return true; 82 | } 83 | return f.getName().toLowerCase().contains( ".replay" ) && f.getName().toLowerCase().contains( ".gbx" ); 84 | } 85 | 86 | @Override public String getDescription() 87 | { 88 | return null; 89 | } 90 | } ); 91 | 92 | JLabel descriptionLabel = new JLabel( "Visualize one or multiple TrackMania replays in charts.

" 93 | + "Each replay's Steering, Throttle " 94 | + "and Brake inputs will be displayed. " 95 | + "The chart will also contain each replay's used input type, the time of the run, the percentage spend on the throttle and brake " 96 | + "and Respawns." ); 97 | panel.add( descriptionLabel ); 98 | descriptionLabel.setBounds( 10, 10, 480, 100 ); 99 | 100 | overlayCb = new JCheckBox( "Overlay in one chart" ); 101 | panel.add( overlayCb ); 102 | overlayCb.setBounds( 10, 120, 170, 25 ); 103 | overlayCb.setToolTipText( "Displays all selected replays in one chart. Only shows the steering movement!" ); 104 | 105 | invertCb = new JCheckBox( "Invert Steering" ); 106 | panel.add( invertCb ); 107 | invertCb.setBounds( 205, 120, 150, 25 ); 108 | invertCb.setToolTipText( "Inverts left and right steering in the chart. Useful to follow along the timeline easier!" ); 109 | 110 | matchTimelineCb = new JCheckBox( "Match Timeline" ); 111 | panel.add( matchTimelineCb ); 112 | matchTimelineCb.setBounds( 360, 120, 160, 25 ); 113 | matchTimelineCb.setToolTipText( "Matches the Timeline (X-Axis) of all selected replays. Eases the comparison of replays!" ); 114 | 115 | invertCb.setSelected( true ); 116 | matchTimelineCb.setSelected( true ); 117 | 118 | JButton selectButton = new JButton( "Select Replays" ); 119 | panel.add( selectButton ); 120 | selectButton.setBounds( 10, 150, 480, 25 ); 121 | selectButton.addActionListener( e -> 122 | { 123 | int returnVal = fc.showDialog( this, "Open" ); 124 | if ( returnVal == 0 ) 125 | { 126 | File[] files = fc.getSelectedFiles(); 127 | 128 | List arguments = new ArrayList<>(); 129 | Arrays.asList( files ).forEach( file -> arguments.add( file.getAbsolutePath() ) ); 130 | 131 | new ReplayChart().init( arguments, overlayCb.isSelected(), invertCb.isSelected(), true, matchTimelineCb.isSelected() ); 132 | } 133 | } ); 134 | } 135 | 136 | /** 137 | * centers the window on the screen 138 | */ 139 | public void centerWindow() 140 | { 141 | Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize(); 142 | int x = (int) ((dimension.getWidth() - this.getWidth()) / 2); 143 | int y = (int) ((dimension.getHeight() - this.getHeight()) / 2); 144 | this.setLocation( x, y ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/gbx/GbxInputExtractor.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart.gbx; 2 | 3 | import de.reilem.replaychart.E_SteeringType; 4 | import de.reilem.replaychart.ReplayData; 5 | import org.anarres.lzo.*; 6 | import org.apache.commons.codec.DecoderException; 7 | import org.apache.commons.codec.binary.Hex; 8 | 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class GbxInputExtractor 16 | { 17 | public static ReplayData parseReplayData( String replayFilePath, boolean invertedSteering ) throws DecoderException, IOException 18 | { 19 | byte[] replayFileBytes = Files.readAllBytes( Paths.get( replayFilePath ) ); 20 | 21 | String replayFileHex = Hex.encodeHexString( replayFileBytes ); 22 | String[] split = replayFileHex.split( "3c2f6865616465723e" ); //split after header 23 | E_TmVersion tmVersion = extractTmVersion( split[0] ); 24 | 25 | //skip reference table 26 | int uncompressedBodySize; 27 | int compressedBodySize; 28 | byte[] compressedBodyBytes; 29 | 30 | if ( tmVersion != E_TmVersion.TM2 ) //eswc & forever 31 | { 32 | uncompressedBodySize = hexToInt( split[1].substring( 16, 24 ) ); //uncompressed body size 33 | compressedBodySize = hexToInt( split[1].substring( 24, 32 ) ); //compressed body size 34 | compressedBodyBytes = Hex.decodeHex( split[1].substring( 32 ) ); //compressed body bytes 35 | } 36 | else 37 | { 38 | int gbxMagicStringLocation = split[1].indexOf( "474258" ); 39 | 40 | int start = gbxMagicStringLocation - 36; 41 | if ( split[1].substring( start, start + 2 ).equals( "00" ) ) 42 | { 43 | start += 2; 44 | } 45 | 46 | uncompressedBodySize = hexToInt( split[1].substring( start, start + 8 ) );//uncompressed body size 47 | compressedBodySize = hexToInt( split[1].substring( start + 8, start + 16 ) ); //compressed body size 48 | compressedBodyBytes = Hex.decodeHex( split[1].substring( start + 16 ) ); //compressed body bytes 49 | } 50 | 51 | byte[] uncompressedBody = decompress( compressedBodyBytes, compressedBodySize, uncompressedBodySize ); 52 | String uncompressedBodyHex = Hex.encodeHexString( uncompressedBody ); //uncompressed body hex 53 | 54 | //Files.write(Paths.get("/home/jedingerd@procilon.local/Downloads/tm2Test"), uncompressedBody); 55 | 56 | String inputMarker = tmVersion == E_TmVersion.FOREVER ? "19200903" : tmVersion == E_TmVersion.ESWC ? "0df00324" : "25200903"; 57 | 58 | String inputBlock = uncompressedBodyHex.split( inputMarker )[1]; //skip to input block 59 | if ( tmVersion == E_TmVersion.TM2 ) 60 | { 61 | inputBlock = inputBlock.substring( 24 ); 62 | } 63 | int replayTime = hexToInt( inputBlock.substring( 0, 8 ) ); //time driven in the replay 64 | int amountOfDifferentInputs = hexToInt( inputBlock.substring( 16, 24 ) ); //amount of different inputs 65 | 66 | List controlNames = new ArrayList<>(); //list of different inputs 67 | 68 | int index = 32; //start of the controlNames list 69 | while ( controlNames.size() < amountOfDifferentInputs ) 70 | { 71 | int length = hexToInt( inputBlock.substring( index, index + 8 ) ); //length of the input string 72 | controlNames.add( hexToAscii( inputBlock.substring( index + 8, index + 8 + (length * 2) ) ) ); //read and add 73 | index = index + 8 + (length * 2); //set index 74 | if ( controlNames.size() < amountOfDifferentInputs ) 75 | { 76 | index += 8; //add index if we are not done yet 77 | } 78 | } 79 | 80 | int amountOfInputs = hexToInt( inputBlock.substring( index, index + 8 ) ); //amount of inputs by player 81 | index += 16; 82 | 83 | List inputs = new ArrayList<>(); 84 | 85 | while ( inputs.size() < amountOfInputs ) 86 | { 87 | GbxSteeringInput in = new GbxSteeringInput(); 88 | 89 | E_GbxInputType type = E_GbxInputType 90 | .getType( controlNames.get( hexToInt( inputBlock.substring( index + 8, index + 10 ) ) ) ); //get type of input 91 | in.setType( type ); 92 | 93 | int value; 94 | if ( in.getType() == E_GbxInputType.STEER ) 95 | { 96 | value = hexToInt24( inputBlock.substring( index + 10, index + 18 ) ); //get pad steer input 97 | } 98 | else 99 | { 100 | value = hexToInt( inputBlock.substring( index + 10, index + 18 ) ); 101 | } 102 | in.setValue( value ); 103 | 104 | int time = hexToInt( inputBlock.substring( index, index + 8 ) ) - 100010; 105 | 106 | if ( tmVersion == E_TmVersion.ESWC ) 107 | { 108 | //round up to nearest 10 in eswc because eswc sometimes uses exact ms times 109 | time = (int) (Math.round( time / 10.0 ) * 10); 110 | } 111 | if ( tmVersion == E_TmVersion.TM2 && time < 0 ) //tm2 events start before 0 112 | { 113 | time = 0; 114 | } 115 | 116 | if ( value != 1.0 && type != E_GbxInputType.STEER && type != E_GbxInputType.FINISH && type != E_GbxInputType.START ) 117 | { 118 | time += 10; 119 | } 120 | in.setTime( time ); // read time + 100010 121 | 122 | inputs.add( in ); 123 | index += 18; 124 | } 125 | 126 | ReplayData replayData = new GbxReplayBuilder().build( replayTime, inputs, invertedSteering, tmVersion ); 127 | replayData.setType( E_SteeringType.DIGITAL ); 128 | 129 | replayData.setFileName( extractReplayName( replayFilePath ) ); 130 | replayData.setTmVersion( tmVersion ); 131 | 132 | return replayData; 133 | } 134 | 135 | private static E_TmVersion extractTmVersion( String header ) 136 | { 137 | String versionText = hexToAscii( header ).split( "exever=\"" )[1]; 138 | 139 | if ( versionText.startsWith( "0." ) ) 140 | { 141 | return E_TmVersion.ESWC; 142 | } 143 | else if ( versionText.startsWith( "2." ) ) 144 | { 145 | return E_TmVersion.FOREVER; 146 | } 147 | else if ( versionText.startsWith( "3." ) ) 148 | { 149 | return E_TmVersion.TM2; 150 | } 151 | else 152 | { 153 | throw new IllegalArgumentException( "Unsupported TM version!" ); 154 | } 155 | } 156 | 157 | private static String extractReplayName( String replayFilePath ) 158 | { 159 | String separator = System.getProperty("file.separator"); 160 | String fileName = replayFilePath.substring( replayFilePath.lastIndexOf( separator ) +1 ); 161 | return fileName.substring( 0, fileName.toLowerCase().lastIndexOf( ".r" ) ); 162 | } 163 | 164 | private static byte[] decompress( byte[] src, int compressedSize, int uncompressedSize ) throws IOException 165 | { 166 | LzoDecompressor decompressor = LzoLibrary.getInstance().newDecompressor( LzoAlgorithm.LZO1X, null ); 167 | byte[] uncompressedBody = new byte[uncompressedSize]; 168 | int lzoReturnCode = decompressor.decompress( src, 0, compressedSize, uncompressedBody, 0, new lzo_uintp( uncompressedSize ) ); 169 | if ( lzoReturnCode == LzoTransformer.LZO_E_OK ) 170 | { 171 | return uncompressedBody; 172 | } 173 | throw new IOException( "Unable to decompress data, lzo code: " + lzoReturnCode ); 174 | } 175 | 176 | private static int hexToInt( String hex ) 177 | { 178 | int index = 0; 179 | StringBuilder reversedHex = new StringBuilder(); 180 | 181 | while ( index + 2 <= hex.length() ) 182 | { 183 | reversedHex.insert( 0, hex, index, index + 2 ); 184 | index = index + 2; 185 | } 186 | 187 | return Integer.parseInt( reversedHex.toString(), 16 ); 188 | } 189 | 190 | private static int hexToInt24( String hex ) throws DecoderException 191 | { 192 | byte[] input = Hex.decodeHex( hex.substring( 0, 6 ) ); //d2fcff 193 | return -((input[2]) << 16 | (input[1] & 0xFF) << 8 | (input[0] & 0xFF)); 194 | } 195 | 196 | private static String hexToAscii( String hexString ) 197 | { 198 | StringBuilder sb = new StringBuilder(); 199 | 200 | for ( int i = 0; i < hexString.length(); i += 2 ) 201 | { 202 | String s = hexString.substring( i, i + 2 ); 203 | sb.append( (char) Integer.parseInt( s, 16 ) ); 204 | } 205 | return sb.toString(); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/donadigo/DonadigoReplayBuilder.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart.donadigo; 2 | 3 | import de.reilem.replaychart.E_SteeringType; 4 | import de.reilem.replaychart.ReplayData; 5 | import de.reilem.replaychart.SteeringAction; 6 | import de.reilem.replaychart.gbx.GbxSteeringInput; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.File; 10 | import java.io.FileReader; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class DonadigoReplayBuilder 15 | { 16 | /** 17 | * read the given file line by line and extract timestamps and steering 18 | * 19 | * @param fileName 20 | * @return 21 | */ 22 | public static ReplayData buildReplay( String fileName, boolean overlaySteering ) 23 | { 24 | ReplayData replayData = new ReplayData(); 25 | 26 | List accelerationList = new ArrayList<>(); 27 | List brakeList = new ArrayList<>(); 28 | List steeringList = new ArrayList<>(); 29 | 30 | int padSteeringCounter = 0; 31 | int keyboardSteeringCounter = 0; 32 | 33 | File file = new File( fileName ); 34 | try ( BufferedReader br = new BufferedReader( new FileReader( file ) ) ) 35 | { 36 | String line; 37 | while ( (line = br.readLine()) != null ) 38 | { 39 | String[] parts = line.split( " " ); 40 | 41 | if ( isInteger( parts[0] ) ) //pad steering 42 | { 43 | steeringList.add( new SteeringAction( parts[0], parts[2] ) ); 44 | padSteeringCounter++; 45 | } 46 | else if ( parts[0].contains( "-" ) && (parts[2].equals( "left" ) || parts[2].equals( "right" )) ) //keyboard steering 47 | { 48 | steeringList.add( new SteeringAction( parts[0], parts[2] ) ); 49 | keyboardSteeringCounter++; 50 | } 51 | else if ( parts[2].equals( "up" ) ) //acceleration 52 | { 53 | accelerationList.add( parts[0] ); 54 | } 55 | else if ( parts[2].equals( "down" ) ) //brake 56 | { 57 | brakeList.add( parts[0] ); 58 | } 59 | } 60 | 61 | calculateReplayLength( steeringList, accelerationList, brakeList, replayData ); 62 | 63 | for ( int i = 0; i < replayData.getReplayTime(); i = i + 10 ) //add a timestamp for the x-axis for every 10ms to please XChart 64 | { 65 | replayData.addTimestamp( i ); 66 | } 67 | 68 | if ( padSteeringCounter != 0 && keyboardSteeringCounter != 0 ) 69 | { 70 | System.err.println( "Mixed ( Pad + Keyboard ) runs are not supported yet!" ); 71 | return new ReplayData(); 72 | } 73 | else if ( padSteeringCounter > keyboardSteeringCounter ) 74 | { 75 | replayData.setType( E_SteeringType.DIGITAL ); 76 | extractPadSteering( steeringList, replayData ); 77 | } 78 | else 79 | { 80 | replayData.setType( E_SteeringType.ANALOG ); 81 | extractKeyboardSteering( steeringList, replayData ); 82 | } 83 | 84 | if ( !overlaySteering ) // don't show acceleration and brake in overlay mode 85 | { 86 | extractAccelerationOrBrake( true, accelerationList, replayData ); 87 | extractAccelerationOrBrake( false, brakeList, replayData ); 88 | } 89 | 90 | 91 | String separator = System.getProperty("file.separator"); 92 | String[] fileNameParts = fileName.split( separator ); 93 | replayData.setFileName( fileNameParts[fileNameParts.length - 1] ); 94 | } 95 | catch ( Throwable e ) 96 | { 97 | e.printStackTrace(); 98 | } 99 | 100 | return replayData; 101 | } 102 | 103 | /** 104 | * extracts the steering inputs 105 | * 106 | * @param steeringList 107 | * @param replayData 108 | */ 109 | private static void extractPadSteering( List steeringList, ReplayData replayData ) 110 | { 111 | int lastTimeStamp = 0; 112 | double lastSteering = 0.0; 113 | 114 | for ( SteeringAction input : steeringList ) 115 | { 116 | int currentTime = Integer.parseInt( input.getTime() ); 117 | 118 | while ( lastTimeStamp + 10 < currentTime ) //timestamps are missing 119 | { 120 | replayData.addSteering( lastSteering ); 121 | lastTimeStamp = lastTimeStamp + 10; 122 | } 123 | 124 | lastSteering = Integer.parseInt( input.getValue() ); 125 | replayData.addSteering( lastSteering ); 126 | lastTimeStamp = currentTime; 127 | } 128 | 129 | while ( replayData.getSteeringLegth() * 10 < replayData.getReplayTime() ) //fill rest of the replay with same steering 130 | { 131 | replayData.addSteering( lastSteering ); 132 | } 133 | } 134 | 135 | /** 136 | * extracts the steering input 137 | * 138 | * @param steeringList 139 | * @param replayData 140 | */ 141 | private static void extractKeyboardSteering( List steeringList, ReplayData replayData ) 142 | { 143 | int lastTimeStamp = 0; 144 | 145 | for ( SteeringAction input : steeringList ) 146 | { 147 | double steer = 0; 148 | 149 | int startTime = Integer.parseInt( input.getTime().split( "-" )[0] ); 150 | int endTime = Integer.parseInt( input.getTime().split( "-" )[1] ); 151 | 152 | while ( lastTimeStamp + 10 < startTime ) //fill the time that passed since the last steering command with no steering 153 | { 154 | replayData.addSteering( steer ); 155 | lastTimeStamp = lastTimeStamp + 10; 156 | } 157 | 158 | if ( startTime <= lastTimeStamp ) //left and right are pressed at the same time 159 | { 160 | startTime = lastTimeStamp + 10; //start when only one button is pressed 161 | //TODO not accurate, since pressing both left and right at the same time results in going LEFT 162 | } 163 | 164 | steer = input.getValue().equals( "left" ) ? -65536 : 65536; // keyboards can only fullsteer 165 | for ( int i = startTime; i <= endTime; i = i + 10 ) 166 | { 167 | replayData.addSteering( steer ); 168 | } 169 | if ( endTime > lastTimeStamp ) 170 | { 171 | lastTimeStamp = endTime; 172 | } 173 | } 174 | 175 | while ( replayData.getSteeringLegth() * 10 < replayData.getReplayTime() ) //fill rest of the replay with no steering 176 | { 177 | replayData.addSteering( 0.0 ); 178 | } 179 | } 180 | 181 | /** 182 | * finds the latest timestamp in the input 183 | * 184 | * @param steeringList 185 | * @param accelerationList 186 | * @param brakeList 187 | * @param replayData 188 | */ 189 | private static void calculateReplayLength( List steeringList, List accelerationList, List brakeList, 190 | ReplayData replayData ) 191 | { 192 | SteeringAction lastSteerPair = steeringList.get( steeringList.size() - 1 ); 193 | String lastAccelerationString = accelerationList.get( accelerationList.size() - 1 ); 194 | String lastBrakeString = brakeList.get( brakeList.size() - 1 ); 195 | 196 | int lastAcceleration = Integer.parseInt( lastAccelerationString.split( "-" )[1] ); 197 | int lastBrake = Integer.parseInt( lastBrakeString.split( "-" )[1] ); 198 | 199 | int lastSteer = 0; 200 | if ( lastSteerPair.getTime().contains( "-" ) ) // keyboard 201 | { 202 | lastSteer = Integer.parseInt( lastSteerPair.getTime().split( "-" )[1] ); 203 | } 204 | else //pad 205 | { 206 | lastSteer = Integer.parseInt( lastSteerPair.getTime() ); 207 | } 208 | 209 | replayData.setReplayTime( Math.max( Math.max( lastAcceleration, lastBrake ), lastSteer ) ); 210 | } 211 | 212 | /** 213 | * extract the brake / acceleration inputs 214 | * 215 | * @param isAcceleration 216 | * @param inputList 217 | * @param replayData 218 | */ 219 | private static void extractAccelerationOrBrake( boolean isAcceleration, List inputList, ReplayData replayData ) 220 | { 221 | int lastTime = 0; 222 | 223 | if ( inputList.isEmpty() && isAcceleration ) 224 | { 225 | inputList.add( "0-0" ); //ESWC runs sometimes? don't have acceleration at all, assume it was pressed all the time 226 | } 227 | 228 | for ( String s : inputList ) 229 | { 230 | int startTime = Integer.parseInt( s.split( "-" )[0] ); 231 | int endTime = Integer.parseInt( s.split( "-" )[1] ); 232 | 233 | if ( lastTime < startTime ) //input doesn't start at 0 || input hasn't been pressed in a while 234 | { 235 | while ( lastTime < startTime ) 236 | { 237 | if ( isAcceleration ) 238 | { 239 | replayData.addAcceleration( 0.0 ); 240 | } 241 | else 242 | { 243 | replayData.addBrake( 0.0 ); 244 | } 245 | lastTime = lastTime + 10; 246 | } 247 | } 248 | 249 | //ESWC runs that press acceleration in the complete run show 0-0 250 | if ( endTime == 0 || endTime > replayData.getReplayTime() ) 251 | { 252 | endTime = replayData.getReplayTime(); 253 | } 254 | 255 | if ( endTime != 0 ) //key is not pressed until the end of the run 256 | { 257 | while ( lastTime < endTime ) 258 | { 259 | if ( isAcceleration ) 260 | { 261 | replayData.addAcceleration( GbxSteeringInput.MAX ); 262 | } 263 | else 264 | { 265 | replayData.addBrake( GbxSteeringInput.MIN ); 266 | } 267 | 268 | lastTime = lastTime + 10; 269 | } 270 | } 271 | 272 | if ( inputList.indexOf( s ) == inputList.size() - 1 && endTime < replayData 273 | .getReplayTime() ) //is the last input && run is not over yet 274 | { 275 | while ( lastTime < replayData.getReplayTime() ) //fill rest of the run with no input 276 | { 277 | if ( isAcceleration ) 278 | { 279 | replayData.addAcceleration( 0.0 ); 280 | } 281 | else 282 | { 283 | replayData.addBrake( 0.0 ); 284 | } 285 | lastTime = lastTime + 10; 286 | } 287 | } 288 | } 289 | } 290 | 291 | /** 292 | * checks if string is integer 293 | * 294 | * @param s 295 | * @return 296 | */ 297 | public static boolean isInteger( String s ) 298 | { 299 | try 300 | { 301 | Integer.parseInt( s ); 302 | } 303 | catch ( Throwable e ) 304 | { 305 | return false; 306 | } 307 | return true; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/main/java/de/reilem/replaychart/ReplayChart.java: -------------------------------------------------------------------------------- 1 | package de.reilem.replaychart; 2 | 3 | import de.reilem.replaychart.donadigo.DonadigoReplayBuilder; 4 | import de.reilem.replaychart.gbx.E_TmVersion; 5 | import de.reilem.replaychart.gbx.GbxInputExtractor; 6 | import de.reilem.replaychart.gbx.GbxSteeringInput; 7 | import org.knowm.xchart.*; 8 | import org.knowm.xchart.style.Styler; 9 | import org.knowm.xchart.style.markers.SeriesMarkers; 10 | 11 | import javax.imageio.ImageIO; 12 | import javax.swing.*; 13 | import java.awt.*; 14 | import java.awt.event.ComponentAdapter; 15 | import java.awt.event.ComponentEvent; 16 | import java.io.File; 17 | import java.math.BigDecimal; 18 | import java.math.RoundingMode; 19 | import java.util.*; 20 | import java.util.List; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | public final class ReplayChart 24 | { 25 | private boolean overlaySteering = false; 26 | private boolean donadigoInput = false; 27 | private boolean invertSteering = false; 28 | private boolean matchTimeline = false; 29 | 30 | private List replays = new ArrayList<>(); 31 | 32 | /** 33 | * read data and show charts 34 | */ 35 | public void init( List arguments, boolean overlaySteering, boolean invertSteering, boolean isUiMode, boolean matchTimeline ) 36 | { 37 | this.overlaySteering = overlaySteering; 38 | this.invertSteering = invertSteering; 39 | this.matchTimeline = matchTimeline; 40 | if ( donadigoInput ) 41 | { 42 | if ( arguments.size() == 1 && new File( arguments.get( 0 ) ).isDirectory() ) //read all files in folder 43 | { 44 | String[] files = new File( arguments.get( 0 ) ).list(); 45 | Arrays.stream( files ) 46 | .forEach( f -> replays.add( DonadigoReplayBuilder.buildReplay( arguments.get( 0 ) + "/" + f, overlaySteering ) ) ); 47 | } 48 | else //read each given file 49 | { 50 | arguments.forEach( arg -> replays.add( DonadigoReplayBuilder.buildReplay( arg, overlaySteering ) ) ); 51 | } 52 | } 53 | else // read gbx 54 | { 55 | if ( arguments.size() == 1 && new File( arguments.get( 0 ) ).isDirectory() ) //read all gbx in folder 56 | { 57 | String[] files = new File( arguments.get( 0 ) ).list(); 58 | Arrays.stream( files ).forEach( f -> 59 | { 60 | if ( f.toLowerCase().endsWith( ".replay.gbx" ) ) 61 | { 62 | try 63 | { 64 | replays.add( GbxInputExtractor.parseReplayData( arguments.get( 0 ) + "/" + f, invertSteering ) ); 65 | } 66 | catch ( Throwable e ) 67 | { 68 | System.out.println( "Unable to extract input from: " + f ); 69 | e.printStackTrace(); 70 | } 71 | } 72 | } ); 73 | } 74 | else 75 | { 76 | arguments.forEach( arg -> 77 | { 78 | try 79 | { 80 | replays.add( GbxInputExtractor.parseReplayData( arg, invertSteering ) ); 81 | } 82 | catch ( Throwable e ) 83 | { 84 | System.out.println( "Unable to extract input from: " + arg ); 85 | e.printStackTrace(); 86 | } 87 | } ); 88 | } 89 | } 90 | 91 | if ( overlaySteering ) //overlay all datasets in one chart 92 | { 93 | XYChart steeringChart = new XYChartBuilder().width( 1440 ).height( 320 ).build(); 94 | steeringChart.getStyler().setTheme( new ReplayTheme() ); 95 | initChart( steeringChart, replays.get( 0 ), -1, -1 ); 96 | steeringChart.getStyler().setLegendPosition( Styler.LegendPosition.InsideNW ); 97 | replays.forEach( r -> 98 | steeringChart.addSeries( r.getChartTitleShort(), r.getTimestamps(), r.getSteering() ).setMarker( SeriesMarkers.NONE ) ); 99 | 100 | List charts = new ArrayList<>(); 101 | charts.add( steeringChart ); 102 | JFrame frame = new SwingWrapper( charts, charts.size(), 1 ).setTitle( "Replay Chart" ).displayChartMatrix(); 103 | 104 | frame.setMinimumSize( new Dimension( 900, 320 ) ); 105 | resizeLegend( frame, steeringChart, replays.size() ); 106 | if ( isUiMode ) 107 | { 108 | try 109 | { 110 | frame.setIconImage( ImageIO.read( 111 | Thread.currentThread().getContextClassLoader().getResourceAsStream( "icon.png" ) ) ); 112 | } 113 | catch ( Throwable e ) 114 | { 115 | //ignore it 116 | } 117 | javax.swing.SwingUtilities.invokeLater( 118 | () -> frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ) 119 | ); 120 | } 121 | 122 | frame.addComponentListener( new ComponentAdapter() 123 | { 124 | public void componentResized( ComponentEvent evt ) 125 | { 126 | resizeLegend( frame, steeringChart, replays.size() ); 127 | } 128 | } ); 129 | } 130 | else //render every dataset in separate chart 131 | { 132 | List charts = new ArrayList<>(); 133 | 134 | int fastestTime = replays.get( 0 ).getReplayTime(); 135 | int slowestTime = replays.get( 0 ).getReplayTime(); 136 | for( ReplayData r : replays ) 137 | { 138 | fastestTime = r.getReplayTime() < fastestTime ? r.getReplayTime() : fastestTime; 139 | slowestTime = r.getReplayTime() > slowestTime ? r.getReplayTime() : slowestTime; 140 | } 141 | 142 | List timestampsLong = new ArrayList<>(); 143 | if( matchTimeline ) //create new timestamp list equal for all replays 144 | { 145 | double tsIndex = 0.0; 146 | while ( tsIndex < slowestTime ) 147 | { 148 | timestampsLong.add( tsIndex ); 149 | tsIndex += 10; 150 | } 151 | } 152 | 153 | //sort by time 154 | Collections.sort( replays, ( r1, r2 ) -> r1.getReplayTime() == r2.getReplayTime() ? 0 : r1.getReplayTime() < r2.getReplayTime() ? -1 : 1 ); 155 | 156 | for( ReplayData r : replays ) 157 | { 158 | XYChart chart = new XYChartBuilder().width( 1440 ).height( 200 ).build(); 159 | chart.getStyler().setTheme( new ReplayTheme() ); 160 | initChart( chart, r, fastestTime, charts.size() ); 161 | chart.getStyler().setLegendVisible( false ); 162 | chart.setTitle( r.getChartTitle() ); 163 | 164 | if ( r.getAcceleration() != null ) 165 | { 166 | if( r.getTmVersion() == E_TmVersion.TM2 ) 167 | { 168 | slowestTime = slowestTime - ( slowestTime % 10 ); 169 | } 170 | if( matchTimeline && r.getReplayTime() < slowestTime ) 171 | { 172 | int index = r.getReplayTime(); 173 | 174 | while( index < slowestTime ) 175 | { 176 | r.addAcceleration( 0.0 ); 177 | index += 10; 178 | } 179 | } 180 | 181 | //create chart, use universal timestampList in align mode 182 | XYSeries accelerationSeries = chart.addSeries( "Acceleration", matchTimeline ? ReplayData.listToArray( timestampsLong ) : r.getTimestamps(), r.getAcceleration() ); 183 | accelerationSeries.setXYSeriesRenderStyle( XYSeries.XYSeriesRenderStyle.Area ); 184 | accelerationSeries.setMarker( SeriesMarkers.NONE ); 185 | accelerationSeries.setFillColor( new Color( 180, 255, 160 ) ); 186 | accelerationSeries.setLineColor( new Color( 0, 0, 0, 0 ) ); 187 | } 188 | 189 | if ( !overlaySteering && r.getBrake() != null ) 190 | { 191 | XYSeries brakeSeries = chart.addSeries( "Brake", r.getTimestamps(), r.getBrake() ); 192 | brakeSeries.setXYSeriesRenderStyle( XYSeries.XYSeriesRenderStyle.Area ); 193 | brakeSeries.setMarker( SeriesMarkers.NONE ); 194 | brakeSeries.setFillColor( new Color( 255, 180, 160 ) ); 195 | brakeSeries.setLineColor( new Color( 0, 0, 0, 0 ) ); 196 | } 197 | 198 | if ( !overlaySteering && r.getSteering() != null ) 199 | { 200 | XYSeries steeringSeries = chart.addSeries( "Steering", r.getTimestamps(), r.getSteering() ); 201 | steeringSeries.setLineWidth( 1.4f ); 202 | steeringSeries.setMarker( SeriesMarkers.NONE ); 203 | steeringSeries.setLineColor( Color.BLACK ); 204 | } 205 | if ( !overlaySteering && r.getRespawns().size() > 0 ) 206 | { 207 | r.getRespawns().forEach( time -> drawRespawn( chart, time ) ); 208 | } 209 | 210 | initCustomLegend( chart, r, matchTimeline ? slowestTime : r.getReplayTime() ); 211 | charts.add( chart ); 212 | } 213 | 214 | JFrame frame = new SwingWrapper( charts, charts.size(), 1 ).setTitle( "Replay Chart" ).displayChartMatrix(); 215 | frame.setMinimumSize( new Dimension( 1000, 160 * replays.size() ) ); 216 | if ( isUiMode ) 217 | { 218 | try 219 | { 220 | frame.setIconImage( ImageIO.read( 221 | Thread.currentThread().getContextClassLoader().getResourceAsStream( "icon.png" ) ) ); 222 | } 223 | catch ( Throwable e ) 224 | { 225 | //ignore it 226 | } 227 | javax.swing.SwingUtilities.invokeLater( 228 | () -> frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ) 229 | ); 230 | } 231 | 232 | frame.addComponentListener( new ComponentAdapter() 233 | { 234 | public void componentResized( ComponentEvent evt ) 235 | { 236 | if ( frame.getHeight() > 300 * replays.size() ) 237 | { 238 | frame.resize( new Dimension( frame.getWidth(), 300 * replays.size() ) ); 239 | } 240 | } 241 | } ); 242 | } 243 | } 244 | 245 | private void drawRespawn( XYChart chart, Integer time ) 246 | { 247 | AnnotationLine respwnLine = new AnnotationLine( time, true, false ); 248 | respwnLine.setColor( new Color( 0, 100, 100, 180 ) ); 249 | chart.addAnnotation( respwnLine ); 250 | 251 | AnnotationText textR = new AnnotationText( " █ Respawn", time, GbxSteeringInput.MAX + 33000, false ); 252 | textR.setFontColor( new Color( 0, 100, 100, 255 ) ); 253 | textR.setTextFont( new Font( Font.MONOSPACED, 0, 10 ) ); 254 | chart.addAnnotation( textR ); 255 | } 256 | 257 | /** 258 | * resizes the max/min of the x-axis to fit the legend in the graph 259 | * 260 | * @param frame 261 | * @param chart 262 | * @param size 263 | */ 264 | private void resizeLegend( JFrame frame, XYChart chart, int size ) 265 | { 266 | int height = frame.getHeight(); 267 | if ( height < 400 ) 268 | { 269 | chart.getStyler().setYAxisMax( GbxSteeringInput.MAX + ((10000000 * size) / frame.getHeight()) ); 270 | } 271 | else if ( height < 500 ) 272 | { 273 | chart.getStyler().setYAxisMax( GbxSteeringInput.MAX + ((7250000 * size) / frame.getHeight()) ); 274 | } 275 | else 276 | { 277 | chart.getStyler().setYAxisMax( GbxSteeringInput.MAX + ((5000000 * size) / frame.getHeight()) ); 278 | } 279 | } 280 | 281 | /** 282 | * add acceleration and brake legend 283 | * @param chart 284 | * @param r 285 | * @param time 286 | */ 287 | private void initCustomLegend( XYChart chart, ReplayData r, int time ) 288 | { 289 | AnnotationText accelerationLegend; 290 | AnnotationText brakeLegend; 291 | AnnotationText deviceLegend; 292 | AnnotationText timeOnLegend; 293 | double offset = (r.getReplayTime() / 100) * 2; 294 | 295 | accelerationLegend = new AnnotationText( "Throttle", time + offset, GbxSteeringInput.MAX - 10000, false ); 296 | brakeLegend = new AnnotationText( "Brake", time + offset, GbxSteeringInput.MIN + 10000, false ); 297 | 298 | deviceLegend = new AnnotationText( 299 | "Input: [" + r.getType() + "] Time: [" + formatTime( (double) r.getReplayTime(), r.getTmVersion() ) + "]" 300 | , time / 6, GbxSteeringInput.MAX + 16000, false ); 301 | 302 | timeOnLegend = new AnnotationText( 303 | r.getPercentTimeOnThrottle() + " " + r.getPercentTimeOnBrake() 304 | , r.getReplayTime() - time / 6, GbxSteeringInput.MAX + 16000, false ); 305 | 306 | accelerationLegend.setFontColor( new Color( 60, 150, 40 ) ); 307 | brakeLegend.setFontColor( new Color( 200, 80, 60 ) ); 308 | deviceLegend.setFontColor( Color.BLACK ); 309 | timeOnLegend.setFontColor( Color.BLACK ); 310 | 311 | chart.addAnnotation( accelerationLegend ); 312 | chart.addAnnotation( brakeLegend ); 313 | chart.addAnnotation( deviceLegend ); 314 | chart.addAnnotation( timeOnLegend ); 315 | } 316 | 317 | /** 318 | * style the given chart 319 | * 320 | * @param chart 321 | * @param r 322 | */ 323 | private void initChart( XYChart chart, ReplayData r, int fastestTime, int replayIndex ) 324 | { 325 | chart.setXAxisTitle( "Time (s)" ); 326 | chart.setYAxisTitle( "Steering" ); 327 | chart.getStyler().setYAxisTicksVisible( false ); 328 | chart.getStyler().setYAxisTitleVisible( false ); 329 | chart.getStyler().setXAxisTitleVisible( false ); 330 | chart.getStyler().setYAxisMax( GbxSteeringInput.MAX ); 331 | chart.getStyler().setYAxisMin( GbxSteeringInput.MIN ); 332 | 333 | chart.getStyler().setxAxisTickLabelsFormattingFunction( aDouble -> formatTimeFullSecond( aDouble, r ) ); 334 | 335 | double offset = -(r.getReplayTime() / 100) * 3; 336 | if ( invertSteering ) 337 | { 338 | chart.addAnnotation( new AnnotationText( "Left", offset, GbxSteeringInput.MAX - 10000, false ) ); 339 | chart.addAnnotation( new AnnotationText( "Right", offset, GbxSteeringInput.MIN + 10000, false ) ); 340 | } 341 | else 342 | { 343 | chart.addAnnotation( new AnnotationText( "Right", offset, GbxSteeringInput.MAX - 10000, false ) ); 344 | chart.addAnnotation( new AnnotationText( "Left", offset, GbxSteeringInput.MIN + 10000, false ) ); 345 | } 346 | chart.addAnnotation( new AnnotationText( "Steering", offset, 0, false ) ); 347 | chart.getStyler().setXAxisMin( r.getReplayTime() < 30000 ? -500.0 : -1000.0 ); 348 | 349 | //min max lines 350 | AnnotationLine maxY = new AnnotationLine( GbxSteeringInput.MAX, false, false ); 351 | maxY.setColor( new Color( 0, 0, 0, 40 ) ); 352 | chart.addAnnotation( maxY ); 353 | 354 | AnnotationLine minY = new AnnotationLine( GbxSteeringInput.MIN, false, false ); 355 | minY.setColor( new Color( 0, 0, 0, 40 ) ); 356 | chart.addAnnotation( minY ); 357 | 358 | AnnotationLine zeroY = new AnnotationLine( 0, false, false ); 359 | zeroY.setColor( new Color( 0, 0, 0, 40 ) ); 360 | chart.addAnnotation( zeroY ); 361 | 362 | AnnotationLine minX = new AnnotationLine( 0, true, false ); 363 | minX.setColor( new Color( 0, 0, 0, 40 ) ); 364 | chart.addAnnotation( minX ); 365 | 366 | AnnotationLine maxX = new AnnotationLine( r.getReplayTime(), true, false ); 367 | maxX.setColor( new Color( 0, 0, 0, 40 ) ); 368 | chart.addAnnotation( maxX ); 369 | 370 | if( fastestTime != -1 ) 371 | { 372 | AnnotationLine fastestTimeLine = new AnnotationLine( fastestTime, true, false ); 373 | fastestTimeLine.setColor( new Color( 70, 0, 160, 170 ) ); 374 | fastestTimeLine.setStroke( new BasicStroke(1.0f) ); 375 | chart.addAnnotation( fastestTimeLine ); 376 | 377 | if( replayIndex == 0 )// fastest replay 378 | { 379 | String spacer = r.getTmVersion() == E_TmVersion.TM2 ? " " : " "; 380 | AnnotationText fastestTimeText = new AnnotationText( spacer + "█ " + formatTime( (double) r.getReplayTime(), r.getTmVersion() ) + "", 381 | fastestTime, GbxSteeringInput.MAX + 33000, false ); 382 | fastestTimeText.setFontColor( new Color( 60, 0, 130, 255 ) ); 383 | fastestTimeText.setTextFont( new Font( Font.MONOSPACED, 0, 10 ) ); 384 | chart.addAnnotation( fastestTimeText ); 385 | } 386 | } 387 | } 388 | 389 | /** 390 | * formats the time 391 | * 392 | * @param aDouble 393 | * @return 394 | */ 395 | public static String formatTime( Double aDouble, E_TmVersion tmVersion ) 396 | { 397 | long m = TimeUnit.MILLISECONDS.toMinutes( aDouble.intValue() ); 398 | long s = TimeUnit.MILLISECONDS.toSeconds( aDouble.intValue() ) - TimeUnit.MINUTES.toSeconds( m ); 399 | long ms = aDouble.intValue() - (TimeUnit.MINUTES.toMillis( m ) + TimeUnit.SECONDS.toMillis( s )); 400 | 401 | if( tmVersion != E_TmVersion.TM2 ) 402 | { 403 | ms = ms/10; 404 | } 405 | 406 | return String.format( "%d:%d.%d", m, s, ms ); 407 | } 408 | 409 | /** 410 | * formats the time - full second 411 | * 412 | * @param aDouble 413 | * @param r 414 | * @return 415 | */ 416 | public static String formatTimeFullSecond( Double aDouble, ReplayData r ) 417 | { 418 | String value = aDouble.toString(); 419 | if ( value.equals( "0.0" ) ) 420 | { 421 | return "0 s"; 422 | } 423 | else if ( value.startsWith( "-" ) ) 424 | { 425 | return " "; 426 | } 427 | 428 | value = value.replaceAll( "0\\.0", "" ); //cut the 0.0 429 | int length = value.length(); 430 | 431 | if ( length < 3 ) 432 | { 433 | return value; 434 | } 435 | return value.substring( 0, length - 2 ) + " s"; 436 | } 437 | 438 | /** 439 | * rounds to two decimal places 440 | * 441 | * @param value 442 | * @return 443 | */ 444 | public static double roundDoubleTwoDecimalPlaces( double value ) 445 | { 446 | try 447 | { 448 | BigDecimal bd = BigDecimal.valueOf( value ); 449 | bd = bd.setScale( 2, RoundingMode.HALF_UP ); 450 | return bd.doubleValue(); 451 | } 452 | catch ( Throwable e ) 453 | { 454 | return 0.0; 455 | } 456 | } 457 | } --------------------------------------------------------------------------------