├── .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 | 
24 |
25 | ## Example 2 (TMN ESWC - reRun)
26 |
27 | 
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 | }
--------------------------------------------------------------------------------