-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rework of parsing the header of a replay (#143)
* Separate out the reading and interpreting of the file * Undo lfs of fafreplay * Fix replays that were damaged by LFS * Make the memory-based functions private * Clean up of the code * Improve documentation of unknown values * Document the header of a replay * First working end-to-end example of the new replay parsing * Restructure the project, process feedback by Sheikah in #142 * Extend tests and fix a few bugs * Discover the byte that is set when queueing orders * Introduce the first semantics * Add interpretation of chat messages * Introduce additional semantics * Use atomics * Rework the enums related to game options * Remove excessive whitespace * Add documentation that it still requires to be implemented * Fix typo in name * Rename 'Utils' to 'LoadUtils' * Use a better describing exception * Undo formatting changes * Remove the tokenizer of the header * Extend pattern matching of modern Java * DO THAT SCREAMING SNAKE CASE THING * Process feedback --------- Co-authored-by: Sheikah45 <[email protected]>
- Loading branch information
Showing
32 changed files
with
1,028 additions
and
169 deletions.
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
15
faf-commons-data/src/main/java/com/faforever/commons/replay/RegisteredEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.faforever.commons.replay; | ||
|
||
import com.faforever.commons.replay.body.Event; | ||
import com.faforever.commons.replay.header.Source; | ||
|
||
/** | ||
* Combines the tick and source of an event. The tick represents when the event was registered. The source represents who authorised the event. | ||
* | ||
* @param tick | ||
* @param source | ||
* @param event | ||
* @see Event | ||
*/ | ||
public record RegisteredEvent(int tick, Source source, Event event) { | ||
} |
15 changes: 15 additions & 0 deletions
15
faf-commons-data/src/main/java/com/faforever/commons/replay/ReplayContainer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.faforever.commons.replay; | ||
|
||
import com.faforever.commons.replay.header.ReplayHeader; | ||
|
||
import java.util.List; | ||
|
||
/** | ||
* A container of all the information that a replay may hold once parsed. | ||
* | ||
* @param metadata | ||
* @param header | ||
* @param registeredEvents | ||
*/ | ||
public record ReplayContainer(ReplayMetadata metadata, ReplayHeader header, List<RegisteredEvent> registeredEvents) { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
faf-commons-data/src/main/java/com/faforever/commons/replay/ReplayLoader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package com.faforever.commons.replay; | ||
|
||
import com.faforever.commons.replay.body.Event; | ||
import com.faforever.commons.replay.body.ReplayBodyParser; | ||
import com.faforever.commons.replay.body.ReplayBodyToken; | ||
import com.faforever.commons.replay.body.ReplayBodyTokenizer; | ||
import com.faforever.commons.replay.header.*; | ||
import com.fasterxml.jackson.databind.DeserializationFeature; | ||
import com.google.common.io.BaseEncoding; | ||
import com.google.common.io.LittleEndianDataInputStream; | ||
import org.apache.commons.compress.compressors.CompressorException; | ||
import org.apache.commons.compress.compressors.CompressorInputStream; | ||
import org.apache.commons.compress.compressors.CompressorStreamFactory; | ||
import org.apache.commons.compress.utils.IOUtils; | ||
import org.jetbrains.annotations.Contract; | ||
import org.jetbrains.annotations.NotNull; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.ByteArrayOutputStream; | ||
import java.io.EOFException; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Objects; | ||
|
||
public class ReplayLoader { | ||
|
||
@Contract(pure = true) | ||
private static ReplayHeader loadSCFAReplayHeader(LittleEndianDataInputStream stream) throws IOException { | ||
return ReplayHeaderParser.parse(stream); | ||
} | ||
|
||
@Contract(pure = true) | ||
private static @NotNull List<RegisteredEvent> loadSCFAReplayBody(List<Source> sources, LittleEndianDataInputStream stream) throws IOException { | ||
List<ReplayBodyToken> bodyTokens = ReplayBodyTokenizer.tokenize(stream); | ||
List<Event> bodyEvents = ReplayBodyParser.parseTokens(bodyTokens); | ||
return ReplaySemantics.registerEvents(sources, bodyEvents); | ||
} | ||
|
||
@Contract(pure = true) | ||
private static ReplayContainer loadSCFAReplayFromMemory(ReplayMetadata metadata, byte[] scfaReplayBytes) throws IOException { | ||
try (LittleEndianDataInputStream stream = new LittleEndianDataInputStream((new ByteArrayInputStream(scfaReplayBytes)))) { | ||
ReplayHeader replayHeader = loadSCFAReplayHeader(stream); | ||
List<RegisteredEvent> replayBody = loadSCFAReplayBody(replayHeader.sources(), stream); | ||
|
||
if (stream.available() > 0) { | ||
throw new EOFException(); | ||
} | ||
|
||
return new ReplayContainer(metadata, replayHeader, replayBody); | ||
} | ||
} | ||
|
||
public static ReplayContainer loadSCFAReplayFromDisk(Path scfaReplayFile) throws IOException, IllegalArgumentException { | ||
if (!scfaReplayFile.toString().toLowerCase().endsWith("scfareplay")) { | ||
throw new IllegalArgumentException("Unknown file format: " + scfaReplayFile.getFileName()); | ||
} | ||
|
||
byte[] bytes = Files.readAllBytes(scfaReplayFile); | ||
return loadSCFAReplayFromMemory(null, bytes); | ||
} | ||
|
||
@Contract(pure = true) | ||
private static ReplayContainer loadFAFReplayFromMemory(byte[] fafReplayBytes) throws IOException, CompressorException { | ||
int separator = findSeparatorIndex(fafReplayBytes); | ||
byte[] metadataBytes = Arrays.copyOfRange(fafReplayBytes, 0, separator); | ||
String metadataString = new String(metadataBytes, StandardCharsets.UTF_8); | ||
|
||
ObjectMapper parsedMetadata = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | ||
ReplayMetadata replayMetadata = parsedMetadata.readValue(metadataString, ReplayMetadata.class); | ||
|
||
byte[] compressedReplayBytes = Arrays.copyOfRange(fafReplayBytes, separator + 1, fafReplayBytes.length); | ||
byte[] scfaReplayBytes = decompress(compressedReplayBytes, replayMetadata); | ||
|
||
return loadSCFAReplayFromMemory(replayMetadata, scfaReplayBytes); | ||
} | ||
|
||
public static ReplayContainer loadFAFReplayFromDisk(Path fafReplayFile) throws IOException, CompressorException, IllegalArgumentException { | ||
if (!fafReplayFile.toString().toLowerCase().endsWith("fafreplay")) { | ||
throw new IllegalArgumentException("Unknown file format: " + fafReplayFile.getFileName()); | ||
} | ||
|
||
byte[] fafReplayBytes = Files.readAllBytes(fafReplayFile); | ||
return loadFAFReplayFromMemory(fafReplayBytes); | ||
} | ||
|
||
private static int findSeparatorIndex(byte[] replayData) { | ||
int headerEnd; | ||
for (headerEnd = 0; headerEnd < replayData.length; headerEnd++) { | ||
if (replayData[headerEnd] == '\n') { | ||
return headerEnd; | ||
} | ||
} | ||
throw new IllegalArgumentException("Missing separator between replay header and body"); | ||
} | ||
|
||
private static byte[] decompress(byte[] data, @NotNull ReplayMetadata metadata) throws IOException, CompressorException { | ||
CompressionType compressionType = Objects.requireNonNullElse(metadata.getCompression(), CompressionType.QTCOMPRESS); | ||
|
||
switch (compressionType) { | ||
case QTCOMPRESS: { | ||
return QtCompress.qUncompress(BaseEncoding.base64().decode(new String(data))); | ||
} | ||
case ZSTD: { | ||
ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(data); | ||
CompressorInputStream compressorInputStream = new CompressorStreamFactory() | ||
.createCompressorInputStream(arrayInputStream); | ||
|
||
ByteArrayOutputStream out = new ByteArrayOutputStream(); | ||
IOUtils.copy(compressorInputStream, out); | ||
return out.toByteArray(); | ||
} | ||
case UNKNOWN: | ||
default: | ||
throw new IOException("Unknown replay format in replay file"); | ||
} | ||
} | ||
} |
Oops, something went wrong.